savepoint 1.0.1 → 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 (278) hide show
  1. package/.claude/settings.local.json +15 -1
  2. package/.golangci.yml +11 -0
  3. package/.savepoint/Design.md +52 -46
  4. package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T001-init-module.md +1 -1
  5. package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T005-layout.md +1 -1
  6. package/.savepoint/releases/v1/epics/E04-board-components/tasks/T002-card.md +1 -1
  7. package/.savepoint/releases/v1/epics/E04-board-components/tasks/T006-help-overlay.md +1 -1
  8. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/{Design.md → E06-Detail.md} +5 -3
  9. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T002-header-and-dividers.md +1 -1
  10. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T003-footer-status-bar.md +1 -1
  11. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T004-component-refinement.md +1 -1
  12. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T010-auto-refresh-watcher.md +2 -0
  13. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/{Design.md → E01-Detail.md} +9 -1
  14. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/{T007-next-activity-header.md → T001-next-activity-header.md} +13 -12
  15. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T002-rename-epic-design-files.md +9 -9
  16. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T003-rename-release-prd.md +2 -2
  17. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T004-update-instruction-files.md +13 -12
  18. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T005-update-cross-references.md +14 -13
  19. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T006-column-and-detail-scrolling.md +25 -15
  20. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T007-column-focus-border-stability.md +57 -0
  21. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/E02-Audit.md +124 -0
  22. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/{Design.md → E02-Detail.md} +12 -3
  23. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T001-fix-makefile.md +11 -8
  24. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T002-linux-build-target.md +12 -7
  25. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T003-macos-build-target.md +9 -5
  26. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T004-smoke-tests-and-artifacts.md +30 -9
  27. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Audit.md +195 -0
  28. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Detail.md +45 -0
  29. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T001-border-resize-fix.md +40 -0
  30. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T002-next-activity-below-header.md +64 -0
  31. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T003-checkbox-rendering-fix.md +56 -0
  32. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T005-unify-status-glyphs.md +65 -0
  33. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T006-forced-256-color-profile.md +36 -0
  34. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/E04-Audit.md +167 -0
  35. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/E04-Detail.md +51 -0
  36. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T001-sidebar-focusable-navigation.md +65 -0
  37. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T002-epic-detail-overlay.md +73 -0
  38. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T003-epic-status-glyphs.md +73 -0
  39. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Audit.md +237 -0
  40. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Detail.md +54 -0
  41. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T001-update-agents-md.md +45 -0
  42. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T002-update-router-md.md +40 -0
  43. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T003-update-design-md.md +47 -0
  44. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T004-implement-m-hotkey.md +98 -0
  45. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T005-update-help-overlay.md +33 -0
  46. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T006-tests-and-quality-gates.md +62 -0
  47. package/.savepoint/releases/v1.1/epics/E06-audit-command/E06-Audit.md +56 -0
  48. package/.savepoint/releases/v1.1/epics/E06-audit-command/E06-Detail.md +63 -0
  49. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T005-proposals.md +44 -0
  50. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T007-apply-close.md +35 -0
  51. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T009-integration.md +40 -0
  52. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T010-audit-file-migration.md +45 -0
  53. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T011-model-tab-state.md +26 -0
  54. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T012-epic-audit-render.md +33 -0
  55. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T013-handle-tab-keys.md +34 -0
  56. package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T014-tab-indicator.md +33 -0
  57. package/.savepoint/releases/v1.1/epics/E07-init-command/E07-Audit.md +336 -0
  58. package/.savepoint/releases/v1.1/epics/E07-init-command/E07-Detail.md +61 -0
  59. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T001-cli-entrypoint.md +37 -0
  60. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T002-target-validation.md +28 -0
  61. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T003-scaffold-writer.md +46 -0
  62. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T004-atomic-writes.md +27 -0
  63. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T005-magic-prompt.md +25 -0
  64. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T006-clipboard.md +26 -0
  65. package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T007-integration-test.md +26 -0
  66. package/.savepoint/releases/v1.1/epics/E08-board-command/E08-Audit.md +333 -0
  67. package/.savepoint/releases/v1.1/epics/E08-board-command/E08-Detail.md +68 -0
  68. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T001-cli-entrypoint.md +26 -0
  69. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T002-non-tty-fallback.md +27 -0
  70. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T003-tui-app-shell.md +28 -0
  71. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T004-board-model.md +29 -0
  72. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T005-detail-pane.md +27 -0
  73. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T006-status-transitions.md +29 -0
  74. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T007-theme-fallbacks.md +29 -0
  75. package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T008-integration-test.md +27 -0
  76. package/.savepoint/releases/v1.1/epics/E09-doctor-command/E09-Audit.md +207 -0
  77. package/.savepoint/releases/v1.1/epics/E09-doctor-command/E09-Detail.md +65 -0
  78. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T001-cli-entrypoint.md +24 -0
  79. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T002-config-router-validation.md +28 -0
  80. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T003-structure-checks.md +29 -0
  81. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T004-dependency-checks.md +27 -0
  82. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T005-audit-orphan-checks.md +28 -0
  83. package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T006-quality-gates-report.md +31 -0
  84. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/E11-Detail.md +36 -0
  85. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T001-debug-logging.md +25 -0
  86. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T002-increase-debounce.md +21 -0
  87. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T003-error-handling.md +22 -0
  88. package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T004-test-verify.md +29 -0
  89. package/.savepoint/releases/v1.1/epics/E12-validation-fix/E12-Audit.md +444 -0
  90. package/.savepoint/releases/v1.1/epics/E12-validation-fix/E12-Detail.md +45 -0
  91. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T001-default-phase.md +35 -0
  92. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T002-default-status.md +19 -0
  93. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T003-better-errors.md +29 -0
  94. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T004-validate-on-write.md +25 -0
  95. package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T005-tests.md +37 -0
  96. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/E13-Audit.md +118 -0
  97. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/E13-Detail.md +73 -0
  98. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T001-safe-cleanup.md +66 -0
  99. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T002-bug-fixes.md +35 -0
  100. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T003-centralize-duplication.md +60 -0
  101. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T004-infrastructure.md +33 -0
  102. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T005-decompose-update.md +37 -0
  103. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T006-async-io.md +40 -0
  104. package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T007-test-coverage.md +37 -0
  105. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/E14-Audit.md +267 -0
  106. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/E14-Detail.md +54 -0
  107. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T001-group-model.md +39 -0
  108. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T002-data-interfaces.md +42 -0
  109. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T003-discover-orphans.md +33 -0
  110. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T004-epic-panel-headings.md +35 -0
  111. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T005-shell-tokenization.md +27 -0
  112. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T006-unify-enums.md +29 -0
  113. package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T007-testutil-package.md +28 -0
  114. package/.savepoint/releases/v1.1/epics/E15-hardening/E15-Detail.md +43 -0
  115. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T001-benchmarks.md +31 -0
  116. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T002-fuzz-targets.md +28 -0
  117. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T003-debug-flag.md +30 -0
  118. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T004-dist-checksums.md +27 -0
  119. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T005-windows-targets.md +28 -0
  120. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T006-abbreviation-splitting.md +26 -0
  121. package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T007-root-test-allowlist.md +28 -0
  122. package/.savepoint/releases/v1.1/epics/_archived/T001-cli-entrypoint.md +25 -0
  123. package/.savepoint/releases/v1.1/epics/_archived/T002-quality-gates.md +27 -0
  124. package/.savepoint/releases/v1.1/epics/_archived/T003-snapshot.md +27 -0
  125. package/.savepoint/releases/v1.1/epics/_archived/T004-ai-reconcile.md +29 -0
  126. package/.savepoint/releases/v1.1/epics/_archived/T006-tui-review.md +31 -0
  127. package/.savepoint/releases/v1.1/epics/_archived/T008-skip-handling.md +34 -0
  128. package/.savepoint/releases/v1.1/v1.1-PRD.md +139 -0
  129. package/.savepoint/router.md +29 -108
  130. package/AGENTS.md +69 -111
  131. package/Makefile +19 -3
  132. package/README.md +6 -6
  133. package/agent-skills/savepoint-audit/SKILL.md +87 -35
  134. package/agent-skills/savepoint-build-task/SKILL.md +9 -4
  135. package/agent-skills/savepoint-create-plan/SKILL.md +10 -5
  136. package/agent-skills/savepoint-create-task/SKILL.md +44 -31
  137. package/agent-skills/savepoint-draft-prd/SKILL.md +8 -3
  138. package/agent-skills/savepoint-system-design/SKILL.md +8 -3
  139. package/agent_skills_test.go +91 -0
  140. package/cmd/board.go +59 -0
  141. package/cmd/board_test.go +137 -0
  142. package/cmd/doctor.go +53 -0
  143. package/cmd/doctor_test.go +146 -0
  144. package/cmd/init.go +63 -0
  145. package/cmd/init_test.go +104 -0
  146. package/internal/board/board.go +69 -49
  147. package/internal/board/board_test.go +83 -67
  148. package/internal/board/card.go +71 -20
  149. package/internal/board/card_test.go +141 -12
  150. package/internal/board/column.go +77 -11
  151. package/internal/board/column_test.go +63 -13
  152. package/internal/board/detail.go +107 -72
  153. package/internal/board/detail_test.go +117 -26
  154. package/internal/board/epic_panel.go +211 -18
  155. package/internal/board/epic_panel_test.go +637 -14
  156. package/internal/board/help.go +1 -0
  157. package/internal/board/help_test.go +1 -0
  158. package/internal/board/integration_test.go +266 -0
  159. package/internal/board/interfaces.go +65 -0
  160. package/internal/board/interfaces_test.go +114 -0
  161. package/internal/board/io.go +93 -0
  162. package/internal/board/layout.go +12 -2
  163. package/internal/board/layout_test.go +17 -0
  164. package/internal/board/model.go +130 -52
  165. package/internal/board/plain.go +88 -0
  166. package/internal/board/plain_test.go +117 -0
  167. package/internal/board/release.go +1 -9
  168. package/internal/board/release_test.go +6 -6
  169. package/internal/board/render_policy_test.go +77 -0
  170. package/internal/board/status.go +23 -0
  171. package/internal/board/theme.go +24 -0
  172. package/internal/board/theme_test.go +31 -0
  173. package/internal/board/transitions.go +113 -88
  174. package/internal/board/transitions_test.go +164 -141
  175. package/internal/board/tui.go +32 -0
  176. package/internal/board/update.go +472 -94
  177. package/internal/board/update_test.go +447 -0
  178. package/internal/board/util.go +76 -0
  179. package/internal/board/view.go +139 -22
  180. package/internal/board/view_test.go +171 -3
  181. package/internal/board/watch.go +57 -9
  182. package/internal/buildtool/main.go +211 -0
  183. package/internal/buildtool/main_test.go +46 -0
  184. package/internal/data/config.go +17 -3
  185. package/internal/data/config_test.go +49 -0
  186. package/internal/data/discover.go +26 -0
  187. package/internal/data/discover_test.go +34 -10
  188. package/internal/data/errors.go +4 -0
  189. package/internal/data/lifecycle.go +13 -6
  190. package/internal/data/lifecycle_test.go +14 -11
  191. package/internal/data/parser.go +29 -6
  192. package/internal/data/parser_test.go +66 -7
  193. package/internal/data/task.go +1 -0
  194. package/internal/data/write.go +85 -11
  195. package/internal/data/write_test.go +167 -0
  196. package/internal/doctor/checks.go +567 -0
  197. package/internal/doctor/checks_test.go +716 -0
  198. package/internal/doctor/gates.go +193 -0
  199. package/internal/doctor/gates_test.go +166 -0
  200. package/internal/doctor/interfaces.go +64 -0
  201. package/internal/doctor/interfaces_test.go +104 -0
  202. package/internal/doctor/repairs.go +80 -0
  203. package/internal/doctor/repairs_test.go +81 -0
  204. package/internal/doctor/report.go +157 -0
  205. package/internal/doctor/report_test.go +89 -0
  206. package/internal/init/clipboard.go +146 -0
  207. package/internal/init/clipboard_test.go +74 -0
  208. package/internal/init/install.go +16 -0
  209. package/internal/init/integration_test.go +197 -0
  210. package/internal/init/prompt.go +14 -0
  211. package/internal/init/prompt_test.go +77 -0
  212. package/internal/init/scaffold.go +59 -0
  213. package/internal/init/scaffold_test.go +179 -0
  214. package/internal/init/template_freshness_test.go +56 -0
  215. package/internal/init/validate.go +85 -0
  216. package/internal/init/validate_test.go +141 -0
  217. package/internal/init/write.go +73 -0
  218. package/internal/init/write_test.go +91 -0
  219. package/internal/styles/palette.go +3 -3
  220. package/internal/styles/styles.go +39 -12
  221. package/internal/styles/styles_test.go +133 -0
  222. package/internal/testutil/fixture.go +113 -0
  223. package/internal/testutil/fs.go +26 -0
  224. package/main.go +107 -1
  225. package/package.json +2 -2
  226. package/project-audit/audit_report_glm_5.1.md +411 -0
  227. package/project-audit/audit_report_opus_4.6 +406 -0
  228. package/project-audit/consolidated-audit-report.md +456 -0
  229. package/savepoint +0 -0
  230. package/templates/project/.savepoint/Design.md +2 -2
  231. package/templates/project/.savepoint/router.md +15 -14
  232. package/templates/project/AGENTS.md +56 -98
  233. package/templates/project/agent-skills/savepoint-audit/SKILL.md +87 -0
  234. package/templates/project/agent-skills/savepoint-build-task/SKILL.md +44 -0
  235. package/templates/project/agent-skills/savepoint-create-plan/SKILL.md +33 -0
  236. package/templates/project/agent-skills/savepoint-create-task/SKILL.md +44 -0
  237. package/templates/project/agent-skills/savepoint-draft-prd/SKILL.md +37 -0
  238. package/templates/project/agent-skills/savepoint-system-design/SKILL.md +38 -0
  239. package/templates/prompts/audit-reconciliation.prompt.md +35 -30
  240. package/templates/prompts/design.prompt.md +3 -1
  241. package/templates/prompts/epic-design.prompt.md +3 -3
  242. package/templates/prompts/task-breakdown.prompt.md +1 -1
  243. package/templates/prompts/task-building.prompt.md +1 -1
  244. package/templates/prompts/task-planning.prompt.md +1 -1
  245. package/.savepoint/audit/E01-go-setup/proposals.md +0 -166
  246. package/.savepoint/audit/E01-go-setup/snapshot.md +0 -71
  247. package/.savepoint/audit/E01-scaffolding/proposals/AGENTS.md +0 -66
  248. package/.savepoint/audit/E01-scaffolding/proposals/Design.md +0 -210
  249. package/.savepoint/audit/E01-scaffolding/proposals/epic-Design.md +0 -117
  250. package/.savepoint/audit/E01-scaffolding/proposals/quality-review.md +0 -101
  251. package/.savepoint/audit/E01-scaffolding/snapshot.md +0 -54
  252. package/.savepoint/audit/E02-data-model/snapshot.md +0 -128
  253. package/.savepoint/audit/E02-data-readers/proposals.md +0 -123
  254. package/.savepoint/audit/E02-data-readers/snapshot.md +0 -54
  255. package/.savepoint/audit/E03-board-tui-core/proposals.md +0 -146
  256. package/.savepoint/audit/E03-board-tui-core/snapshot.md +0 -57
  257. package/.savepoint/audit/E03-cli-foundation/snapshot.md +0 -106
  258. package/.savepoint/audit/E04-board-components/proposals.md +0 -118
  259. package/.savepoint/audit/E04-board-components/snapshot.md +0 -77
  260. package/.savepoint/audit/E04-templates-and-prompts/snapshot.md +0 -115
  261. package/.savepoint/audit/E05-init-command/snapshot.md +0 -125
  262. package/.savepoint/audit/E05-phase-transitions/proposals.md +0 -83
  263. package/.savepoint/audit/E05-phase-transitions/snapshot.md +0 -36
  264. package/.savepoint/audit/E06-atari-noir-layout/proposals.md +0 -130
  265. package/.savepoint/audit/E06-atari-noir-layout/snapshot.md +0 -84
  266. package/.savepoint/audit/E06-tui-board/snapshot.md +0 -64
  267. package/.savepoint/audit/E07-audit-pipeline/snapshot.md +0 -165
  268. package/.savepoint/audit/E08-board-workflow-cleanup/snapshot.md +0 -65
  269. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T001-border-resize-fix.md +0 -36
  270. package/ink-cli-ui-design.zip +0 -0
  271. package/main.exe +0 -0
  272. package/savepoint.exe +0 -0
  273. /package/.savepoint/releases/v1/epics/E01-go-setup/{Design.md → E01-Detail.md} +0 -0
  274. /package/.savepoint/releases/v1/epics/E02-data-readers/{Design.md → E02-Detail.md} +0 -0
  275. /package/.savepoint/releases/v1/epics/E03-board-tui-core/{Design.md → E03-Detail.md} +0 -0
  276. /package/.savepoint/releases/v1/epics/E04-board-components/{Design.md → E04-Detail.md} +0 -0
  277. /package/.savepoint/releases/v1/epics/E05-phase-transitions/{Design.md → E05-Detail.md} +0 -0
  278. /package/.savepoint/releases/v1/{PRD.md → v1-PRD.md} +0 -0
@@ -1,9 +1,6 @@
1
1
  package board
2
2
 
3
3
  import (
4
- "os"
5
- "path/filepath"
6
-
7
4
  tea "github.com/charmbracelet/bubbletea"
8
5
  "github.com/fsnotify/fsnotify"
9
6
  "github.com/opencode/savepoint/internal/data"
@@ -12,44 +9,102 @@ import (
12
9
  type OverlayType string
13
10
 
14
11
  const (
15
- OverlayNone OverlayType = ""
16
- OverlayHelp OverlayType = "help"
17
- OverlayEpic OverlayType = "epic"
18
- OverlayRelease OverlayType = "release"
19
- OverlayDetail OverlayType = "detail"
12
+ OverlayNone OverlayType = ""
13
+ OverlayHelp OverlayType = "help"
14
+ OverlayEpic OverlayType = "epic"
15
+ OverlayRelease OverlayType = "release"
16
+ OverlayDetail OverlayType = "detail"
17
+ OverlayEpicDetail OverlayType = "detail-epic"
20
18
  )
21
19
 
22
- // Model holds all board state. Tasks are grouped by column for O(1) column access.
23
- type Model struct {
24
- AllTasks []data.Task
25
- Tasks map[data.ColumnType][]data.Task
26
- FocusedColumn data.ColumnType
27
- FocusedTask int
28
- SelectedEpic string
20
+ // ViewConfig holds terminal and overlay presentation state.
21
+ type ViewConfig struct {
22
+ Theme data.Theme
23
+ Overlay OverlayType
24
+ Width int
25
+ Height int
26
+ StatusMessage string
27
+ }
28
+
29
+ // DataState holds task, router, and filesystem state used by the board.
30
+ type DataState struct {
31
+ AllTasks []data.Task
32
+ Tasks map[data.ColumnType][]data.Task
33
+ Root string
34
+ EpicStatus map[string]string
35
+ RouterTask string
36
+ RouterState *data.RouterState
37
+ Watcher *fsnotify.Watcher
38
+ }
39
+
40
+ // NavigationState holds board-column and detail scrolling state.
41
+ type NavigationState struct {
42
+ FocusedColumn data.ColumnType
43
+ FocusedTask int
44
+ ColumnOffsets map[data.ColumnType]int
45
+ DetailOffset int
46
+ }
47
+
48
+ // EpicState holds epic list, sidebar, and detail overlay state.
49
+ type EpicState struct {
50
+ SelectedEpic string
51
+ Epics []string
52
+ EpicCursor int
53
+ EpicPanelFocus bool
54
+ EpicPanelCursor int
55
+ EpicDetailOffset int
56
+ EpicDetailEpic string
57
+ EpicDetailContent string
58
+ EpicDetailTab int // 0=Detail, 1=Audit
59
+ EpicAuditContent string // cached E##-Audit.md content
60
+ }
61
+
62
+ // ReleaseState holds release list and release picker state.
63
+ type ReleaseState struct {
29
64
  SelectedRelease string
30
- Epics []string
31
- EpicCursor int
32
65
  Releases []string
33
66
  ReleaseEpics map[string][]string
34
67
  ReleaseCursor int
35
- Overlay OverlayType
36
- Width int
37
- Height int
38
- StatusMessage string
39
- Root string
40
- RouterTask string
41
- Watcher *fsnotify.Watcher
68
+ }
69
+
70
+ // DataAccessState holds board data-access implementations.
71
+ type DataAccessState struct {
72
+ Dependencies ModelDependencies
73
+ }
74
+
75
+ // Model holds all board state. Tasks are grouped by column for O(1) column access.
76
+ type Model struct {
77
+ ViewConfig
78
+ DataState
79
+ NavigationState
80
+ EpicState
81
+ ReleaseState
82
+ DataAccessState
42
83
  }
43
84
 
44
85
  // NewModel groups tasks by column and returns an initialized Model.
45
- func NewModel(tasks []data.Task, release, epic string) Model {
86
+ func NewModel(tasks []data.Task, release, epic string, deps ...ModelDependencies) Model {
46
87
  m := Model{
47
- AllTasks: append([]data.Task(nil), tasks...),
48
- FocusedColumn: data.ColumnPlanned,
49
- FocusedTask: 0,
50
- SelectedEpic: epic,
51
- SelectedRelease: release,
52
- Overlay: OverlayNone,
88
+ ViewConfig: ViewConfig{
89
+ Overlay: OverlayNone,
90
+ },
91
+ DataState: DataState{
92
+ AllTasks: append([]data.Task(nil), tasks...),
93
+ },
94
+ NavigationState: NavigationState{
95
+ FocusedColumn: data.ColumnPlanned,
96
+ FocusedTask: 0,
97
+ ColumnOffsets: newColumnOffsets(),
98
+ },
99
+ EpicState: EpicState{
100
+ SelectedEpic: epic,
101
+ },
102
+ ReleaseState: ReleaseState{
103
+ SelectedRelease: release,
104
+ },
105
+ DataAccessState: DataAccessState{
106
+ Dependencies: modelDependencies(deps),
107
+ },
53
108
  }
54
109
  m.refreshTasks()
55
110
  return m
@@ -91,6 +146,15 @@ func (m *Model) refreshTasks() {
91
146
  }
92
147
  m.Tasks = groupedTasks(visible)
93
148
  m.clampFocusedTask()
149
+ m.clampColumnOffsets()
150
+ }
151
+
152
+ func newColumnOffsets() map[data.ColumnType]int {
153
+ return map[data.ColumnType]int{
154
+ data.ColumnPlanned: 0,
155
+ data.ColumnInProgress: 0,
156
+ data.ColumnDone: 0,
157
+ }
94
158
  }
95
159
 
96
160
  func (m *Model) refreshEpicsForRelease() {
@@ -103,18 +167,36 @@ func (m *Model) refreshEpicsForRelease() {
103
167
  if len(m.Epics) == 0 {
104
168
  m.SelectedEpic = ""
105
169
  m.EpicCursor = 0
170
+ m.EpicPanelCursor = 0
171
+ m.EpicPanelFocus = false
106
172
  return
107
173
  }
108
174
 
109
175
  for _, epic := range m.Epics {
110
176
  if epic == m.SelectedEpic {
111
- m.EpicCursor = epicIndex(m.Epics, m.SelectedEpic)
177
+ m.EpicCursor = sliceIndex(m.Epics, m.SelectedEpic)
178
+ m.clampEpicPanelCursor()
112
179
  return
113
180
  }
114
181
  }
115
182
 
116
183
  m.SelectedEpic = m.Epics[0]
117
184
  m.EpicCursor = 0
185
+ m.clampEpicPanelCursor()
186
+ }
187
+
188
+ func (m *Model) clampEpicPanelCursor() {
189
+ if len(m.Epics) == 0 {
190
+ m.EpicPanelCursor = 0
191
+ m.EpicPanelFocus = false
192
+ return
193
+ }
194
+ if m.EpicPanelCursor >= len(m.Epics) {
195
+ m.EpicPanelCursor = len(m.Epics) - 1
196
+ }
197
+ if m.EpicPanelCursor < 0 {
198
+ m.EpicPanelCursor = 0
199
+ }
118
200
  }
119
201
 
120
202
  func (m *Model) clampFocusedTask() {
@@ -131,27 +213,23 @@ func (m *Model) clampFocusedTask() {
131
213
  }
132
214
  }
133
215
 
134
- func (m *Model) writeRouterReleaseEpic() error {
135
- routerPath := filepath.Join(m.Root, "router.md")
136
-
137
- fi, err := os.Stat(routerPath)
138
- if err != nil {
139
- return err
140
- }
141
-
142
- content, err := os.ReadFile(routerPath)
143
- if err != nil {
144
- return err
216
+ func (m *Model) clampColumnOffsets() {
217
+ if m.ColumnOffsets == nil {
218
+ m.ColumnOffsets = newColumnOffsets()
145
219
  }
146
-
147
- r := data.NewRouterReader()
148
- state, err := r.ReadState(string(content))
149
- if err != nil {
150
- return err
220
+ for _, col := range columnOrder {
221
+ tasks := m.Tasks[col]
222
+ offset := m.ColumnOffsets[col]
223
+ if offset < 0 || len(tasks) == 0 {
224
+ m.ColumnOffsets[col] = 0
225
+ continue
226
+ }
227
+ if offset >= len(tasks) {
228
+ m.ColumnOffsets[col] = len(tasks) - 1
229
+ }
151
230
  }
231
+ }
152
232
 
153
- state.Epic = m.SelectedEpic
154
- state.Release = m.SelectedRelease
155
-
156
- return data.WriteRouterState(m.Root, state, fi.ModTime())
233
+ func taskDone(task data.Task) bool {
234
+ return task.Column == data.ColumnDone
157
235
  }
@@ -0,0 +1,88 @@
1
+ package board
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+ "path/filepath"
7
+ "strings"
8
+
9
+ "github.com/opencode/savepoint/internal/data"
10
+ )
11
+
12
+ const plainNonTTYWarning = "[non-interactive mode — run in a TTY to launch the board UI]"
13
+ const plainAuditSignal = "[◆ audit proposals pending]"
14
+
15
+ // RenderPlainTable renders a plain text three-column task table for non-TTY output.
16
+ func RenderPlainTable(model Model) string {
17
+ var b strings.Builder
18
+
19
+ fmt.Fprintln(&b, plainNonTTYWarning)
20
+ if hasAuditProposals(model.Root) {
21
+ fmt.Fprintln(&b, plainAuditSignal)
22
+ }
23
+ fmt.Fprintln(&b)
24
+
25
+ cols := []struct {
26
+ label string
27
+ col data.ColumnType
28
+ }{
29
+ {"PLANNED", data.ColumnPlanned},
30
+ {"IN PROGRESS", data.ColumnInProgress},
31
+ {"DONE", data.ColumnDone},
32
+ }
33
+
34
+ for _, c := range cols {
35
+ tasks := model.Tasks[c.col]
36
+ fmt.Fprintln(&b, c.label)
37
+ if len(tasks) == 0 {
38
+ fmt.Fprintln(&b, " (none)")
39
+ }
40
+ for _, t := range tasks {
41
+ title := t.Title
42
+ if title == "" {
43
+ title = "(no title)"
44
+ }
45
+ fmt.Fprintf(&b, " %-52s %s\n", t.ID, title)
46
+ }
47
+ fmt.Fprintln(&b)
48
+ }
49
+
50
+ return b.String()
51
+ }
52
+
53
+ // hasAuditProposals reports whether any audit file under root contains a Proposed Changes section.
54
+ func hasAuditProposals(root string) bool {
55
+ releasesDir := filepath.Join(root, "releases")
56
+ releases, err := os.ReadDir(releasesDir)
57
+ if err != nil {
58
+ return false
59
+ }
60
+ for _, r := range releases {
61
+ if !r.IsDir() {
62
+ continue
63
+ }
64
+ epicsDir := filepath.Join(releasesDir, r.Name(), "epics")
65
+ epics, err := os.ReadDir(epicsDir)
66
+ if err != nil {
67
+ continue
68
+ }
69
+ for _, e := range epics {
70
+ if !e.IsDir() {
71
+ continue
72
+ }
73
+ short := e.Name()
74
+ if idx := strings.Index(short, "-"); idx >= 0 {
75
+ short = short[:idx]
76
+ }
77
+ auditPath := filepath.Join(epicsDir, e.Name(), short+"-Audit.md")
78
+ raw, err := os.ReadFile(auditPath)
79
+ if err != nil {
80
+ continue
81
+ }
82
+ if strings.Contains(string(raw), "## Proposed Changes") {
83
+ return true
84
+ }
85
+ }
86
+ }
87
+ return false
88
+ }
@@ -0,0 +1,117 @@
1
+ package board
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "strings"
7
+ "testing"
8
+
9
+ "github.com/opencode/savepoint/internal/data"
10
+ )
11
+
12
+ func TestRenderPlainTable_warningBanner(t *testing.T) {
13
+ m := NewModel(nil, "", "")
14
+ got := RenderPlainTable(m)
15
+ if !strings.Contains(got, plainNonTTYWarning) {
16
+ t.Errorf("RenderPlainTable missing warning banner, got:\n%s", got)
17
+ }
18
+ }
19
+
20
+ func TestRenderPlainTable_columnHeaders(t *testing.T) {
21
+ m := NewModel(nil, "", "")
22
+ got := RenderPlainTable(m)
23
+ for _, header := range []string{"PLANNED", "IN PROGRESS", "DONE"} {
24
+ if !strings.Contains(got, header) {
25
+ t.Errorf("RenderPlainTable missing column header %q", header)
26
+ }
27
+ }
28
+ }
29
+
30
+ func TestRenderPlainTable_taskIDsAndTitles(t *testing.T) {
31
+ tasks := []data.Task{
32
+ {ID: "E08/T001", Title: "CLI entrypoint", Column: data.ColumnDone},
33
+ {ID: "E08/T002", Title: "Non-TTY fallback", Column: data.ColumnInProgress},
34
+ {ID: "E08/T003", Title: "TUI app shell", Column: data.ColumnPlanned},
35
+ }
36
+ m := NewModel(tasks, "", "")
37
+ got := RenderPlainTable(m)
38
+
39
+ for _, want := range []string{"E08/T001", "CLI entrypoint", "E08/T002", "Non-TTY fallback", "E08/T003", "TUI app shell"} {
40
+ if !strings.Contains(got, want) {
41
+ t.Errorf("RenderPlainTable missing %q", want)
42
+ }
43
+ }
44
+ }
45
+
46
+ func TestRenderPlainTable_noneWhenColumnEmpty(t *testing.T) {
47
+ m := NewModel(nil, "", "")
48
+ got := RenderPlainTable(m)
49
+ if !strings.Contains(got, "(none)") {
50
+ t.Errorf("RenderPlainTable should show (none) for empty columns")
51
+ }
52
+ }
53
+
54
+ func TestRenderPlainTable_auditSignalWhenProposalsExist(t *testing.T) {
55
+ root := t.TempDir()
56
+ epicDir := filepath.Join(root, "releases", "v1", "epics", "E01-test")
57
+ if err := os.MkdirAll(epicDir, 0755); err != nil {
58
+ t.Fatal(err)
59
+ }
60
+ auditContent := "---\ntype: audit\n---\n## Main Findings\nOK\n\n## Proposed Changes\n\n### Target File\nfoo.go\n"
61
+ if err := os.WriteFile(filepath.Join(epicDir, "E01-Audit.md"), []byte(auditContent), 0644); err != nil {
62
+ t.Fatal(err)
63
+ }
64
+
65
+ m := NewModel(nil, "", "")
66
+ m.Root = root
67
+ got := RenderPlainTable(m)
68
+ if !strings.Contains(got, plainAuditSignal) {
69
+ t.Errorf("RenderPlainTable missing audit signal when proposals exist, got:\n%s", got)
70
+ }
71
+ }
72
+
73
+ func TestRenderPlainTable_noAuditSignalWhenNone(t *testing.T) {
74
+ root := t.TempDir()
75
+ m := NewModel(nil, "", "")
76
+ m.Root = root
77
+ got := RenderPlainTable(m)
78
+ if strings.Contains(got, plainAuditSignal) {
79
+ t.Errorf("RenderPlainTable should not show audit signal when no proposals exist")
80
+ }
81
+ }
82
+
83
+ func TestHasAuditProposals_detectsSection(t *testing.T) {
84
+ root := t.TempDir()
85
+ epicDir := filepath.Join(root, "releases", "v1", "epics", "E02-slug")
86
+ if err := os.MkdirAll(epicDir, 0755); err != nil {
87
+ t.Fatal(err)
88
+ }
89
+ content := "## Main Findings\nAll good.\n\n## Proposed Changes\n\n### Target File\nbar.go\n"
90
+ if err := os.WriteFile(filepath.Join(epicDir, "E02-Audit.md"), []byte(content), 0644); err != nil {
91
+ t.Fatal(err)
92
+ }
93
+ if !hasAuditProposals(root) {
94
+ t.Error("hasAuditProposals should return true when Proposed Changes section exists")
95
+ }
96
+ }
97
+
98
+ func TestHasAuditProposals_noProposals(t *testing.T) {
99
+ root := t.TempDir()
100
+ epicDir := filepath.Join(root, "releases", "v1", "epics", "E03-slug")
101
+ if err := os.MkdirAll(epicDir, 0755); err != nil {
102
+ t.Fatal(err)
103
+ }
104
+ content := "## Main Findings\nAll good.\n"
105
+ if err := os.WriteFile(filepath.Join(epicDir, "E03-Audit.md"), []byte(content), 0644); err != nil {
106
+ t.Fatal(err)
107
+ }
108
+ if hasAuditProposals(root) {
109
+ t.Error("hasAuditProposals should return false when no Proposed Changes section")
110
+ }
111
+ }
112
+
113
+ func TestHasAuditProposals_missingRoot(t *testing.T) {
114
+ if hasAuditProposals("/nonexistent/path/xyz") {
115
+ t.Error("hasAuditProposals should return false for missing root")
116
+ }
117
+ }
@@ -31,12 +31,4 @@ func RenderReleaseDropdown(releases []string, cursor int, width int) string {
31
31
  return styles.EpicPanel.Width(width).Render(strings.Join(lines, "\n"))
32
32
  }
33
33
 
34
- // releaseIndex returns the index of selected in releases, or 0 if not found.
35
- func releaseIndex(releases []string, selected string) int {
36
- for i, r := range releases {
37
- if r == selected {
38
- return i
39
- }
40
- }
41
- return 0
42
- }
34
+
@@ -40,17 +40,17 @@ func TestRenderReleaseDropdown_hintsPresent(t *testing.T) {
40
40
  }
41
41
  }
42
42
 
43
- func TestReleaseIndex_found(t *testing.T) {
43
+ func TestSliceIndexRelease_found(t *testing.T) {
44
44
  releases := []string{"v1", "v2", "v3"}
45
- if got := releaseIndex(releases, "v2"); got != 1 {
46
- t.Errorf("releaseIndex = %d, want 1", got)
45
+ if got := sliceIndex(releases, "v2"); got != 1 {
46
+ t.Errorf("sliceIndex = %d, want 1", got)
47
47
  }
48
48
  }
49
49
 
50
- func TestReleaseIndex_notFound(t *testing.T) {
50
+ func TestSliceIndexRelease_notFound(t *testing.T) {
51
51
  releases := []string{"v1", "v2"}
52
- if got := releaseIndex(releases, "v9"); got != 0 {
53
- t.Errorf("releaseIndex = %d, want 0", got)
52
+ if got := sliceIndex(releases, "v9"); got != 0 {
53
+ t.Errorf("sliceIndex = %d, want 0", got)
54
54
  }
55
55
  }
56
56
 
@@ -0,0 +1,77 @@
1
+ package board
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+
7
+ "github.com/charmbracelet/lipgloss"
8
+ "github.com/muesli/termenv"
9
+ "github.com/opencode/savepoint/internal/data"
10
+ )
11
+
12
+ func TestRenderPolicy_noBackgroundEscapes(t *testing.T) {
13
+ lipgloss.SetColorProfile(termenv.ANSI256)
14
+
15
+ task := data.Task{
16
+ ID: "E05-tasking-permissions/T004-implement-m-hotkey",
17
+ Title: "Implement router priority",
18
+ Release: "v1.1",
19
+ Epic: "E05-tasking-permissions",
20
+ Column: data.ColumnInProgress,
21
+ Stage: data.StageBuild,
22
+ }
23
+ m := NewModel([]data.Task{
24
+ {
25
+ ID: task.ID,
26
+ Title: task.Title,
27
+ Release: task.Release,
28
+ Epic: task.Epic,
29
+ Column: task.Column,
30
+ Stage: task.Stage,
31
+ },
32
+ }, "v1.1", "E05-tasking-permissions")
33
+ m.Width = 120
34
+ m.Height = 30
35
+ m.FocusedColumn = data.ColumnInProgress
36
+ m.RouterState = &data.RouterState{
37
+ State: "task-building",
38
+ Release: "v1.1",
39
+ Epic: "E05-tasking-permissions",
40
+ Task: "E05-tasking-permissions/T004-implement-m-hotkey",
41
+ NextAction: "Build E05-tasking-permissions/T004-implement-m-hotkey.",
42
+ }
43
+
44
+ cases := map[string]string{
45
+ "board": m.View(),
46
+ "card": RenderCard(task, 30, true, m.RouterState),
47
+ "detail": RenderDetail(task, 60, m.RouterState, 0, 0),
48
+ "epic dropdown": RenderEpicDropdown([]string{"E05-tasking-permissions"}, 0, 40),
49
+ "release dropdown": RenderReleaseDropdown([]string{"v1.1"}, 0, 40),
50
+ "help": RenderHelp(60),
51
+ }
52
+ for name, got := range cases {
53
+ assertNoBackgroundEscapes(t, name, got)
54
+ }
55
+ }
56
+
57
+ func assertNoBackgroundEscapes(t *testing.T, name, got string) {
58
+ t.Helper()
59
+ for _, escape := range []string{"\x1b[48;", "\x1b[40m"} {
60
+ if strings.Contains(got, escape) {
61
+ t.Fatalf("%s emitted background escape prefix %q in %q", name, escape, got)
62
+ }
63
+ }
64
+ }
65
+
66
+ func TestRenderPolicy_usesSingleLineBorders(t *testing.T) {
67
+ m := NewModel([]data.Task{{ID: "T001", Title: "Task", Column: data.ColumnPlanned}}, "v1", "E01")
68
+ m.Width = 120
69
+ got := m.View()
70
+
71
+ if strings.Contains(got, "╭") || strings.Contains(got, "╮") || strings.Contains(got, "╰") || strings.Contains(got, "╯") {
72
+ t.Fatalf("View should use single-line borders, got rounded border glyphs in %q", got)
73
+ }
74
+ if !strings.Contains(got, "┌") || !strings.Contains(got, "┐") || !strings.Contains(got, "└") || !strings.Contains(got, "┘") {
75
+ t.Fatalf("View missing expected single-line border glyphs in %q", got)
76
+ }
77
+ }
@@ -0,0 +1,23 @@
1
+ package board
2
+
3
+ import (
4
+ "github.com/opencode/savepoint/internal/data"
5
+ "github.com/opencode/savepoint/internal/styles"
6
+ )
7
+
8
+ const statusGlyphDefault = " "
9
+
10
+ func statusGlyph(status string) string {
11
+ switch status {
12
+ case string(data.ColumnPlanned):
13
+ return styles.CardMeta.Render("○")
14
+ case string(data.ColumnInProgress):
15
+ return styles.GlyphBuild.Render("▶")
16
+ case string(data.ColumnDone):
17
+ return styles.TagDone.Render("◉")
18
+ case "audited":
19
+ return styles.TagDone.Render("✓")
20
+ default:
21
+ return statusGlyphDefault
22
+ }
23
+ }
@@ -0,0 +1,24 @@
1
+ package board
2
+
3
+ import (
4
+ "os"
5
+
6
+ "github.com/charmbracelet/lipgloss"
7
+ "github.com/muesli/termenv"
8
+ )
9
+
10
+ // applyColorProfile detects terminal color support and configures lipgloss.
11
+ // Priority: NO_COLOR → COLORTERM env hint → termenv auto-detect.
12
+ func applyColorProfile() {
13
+ lipgloss.SetColorProfile(detectProfile())
14
+ }
15
+
16
+ func detectProfile() termenv.Profile {
17
+ if os.Getenv("NO_COLOR") != "" {
18
+ return termenv.Ascii
19
+ }
20
+ if c := os.Getenv("COLORTERM"); c == "truecolor" || c == "24bit" {
21
+ return termenv.TrueColor
22
+ }
23
+ return termenv.ColorProfile()
24
+ }
@@ -0,0 +1,31 @@
1
+ package board
2
+
3
+ import (
4
+ "testing"
5
+
6
+ "github.com/muesli/termenv"
7
+ )
8
+
9
+ func TestDetectProfileNOCOLOR(t *testing.T) {
10
+ t.Setenv("NO_COLOR", "1")
11
+ t.Setenv("COLORTERM", "")
12
+ if got := detectProfile(); got != termenv.Ascii {
13
+ t.Fatalf("want Ascii, got %v", got)
14
+ }
15
+ }
16
+
17
+ func TestDetectProfileTruecolor(t *testing.T) {
18
+ t.Setenv("NO_COLOR", "")
19
+ t.Setenv("COLORTERM", "truecolor")
20
+ if got := detectProfile(); got != termenv.TrueColor {
21
+ t.Fatalf("want TrueColor, got %v", got)
22
+ }
23
+ }
24
+
25
+ func TestDetectProfile24bit(t *testing.T) {
26
+ t.Setenv("NO_COLOR", "")
27
+ t.Setenv("COLORTERM", "24bit")
28
+ if got := detectProfile(); got != termenv.TrueColor {
29
+ t.Fatalf("want TrueColor, got %v", got)
30
+ }
31
+ }