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.
- package/.claude/settings.local.json +12 -1
- package/.github/workflows/ci.yml +20 -0
- package/.golangci.yml +11 -0
- package/.savepoint/Design.md +40 -38
- package/.savepoint/{audit/v1.1/E02-cross-platform-compatibility/proposals.md → releases/v1.1/epics/E02-cross-platform-compatibility/E02-Audit.md} +48 -38
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Audit.md +195 -0
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Detail.md +14 -1
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T006-forced-256-color-profile.md +3 -3
- package/.savepoint/{audit/v1.1/E04-epic-navigation/proposals.md → releases/v1.1/epics/E04-epic-navigation/E04-Audit.md} +65 -54
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Audit.md +237 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Detail.md +25 -16
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T001-update-agents-md.md +17 -6
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T002-update-router-md.md +15 -5
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T003-update-design-md.md +19 -5
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T004-implement-m-hotkey.md +11 -1
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T005-update-help-overlay.md +9 -6
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T006-tests-and-quality-gates.md +29 -13
- package/.savepoint/releases/v1.1/epics/E06-audit-command/E06-Audit.md +56 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/E06-Detail.md +63 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T005-proposals.md +44 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T007-apply-close.md +35 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T009-integration.md +40 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T010-audit-file-migration.md +45 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T011-model-tab-state.md +26 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T012-epic-audit-render.md +33 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T013-handle-tab-keys.md +34 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T014-tab-indicator.md +33 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/E07-Audit.md +336 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/E07-Detail.md +61 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T001-cli-entrypoint.md +37 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T002-target-validation.md +28 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T003-scaffold-writer.md +46 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T004-atomic-writes.md +27 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T005-magic-prompt.md +25 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T006-clipboard.md +26 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T007-integration-test.md +26 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/E08-Audit.md +333 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/E08-Detail.md +68 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T001-cli-entrypoint.md +26 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T002-non-tty-fallback.md +27 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T003-tui-app-shell.md +28 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T004-board-model.md +29 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T005-detail-pane.md +27 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T006-status-transitions.md +29 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T007-theme-fallbacks.md +29 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T008-integration-test.md +27 -0
- package/.savepoint/releases/v1.1/epics/E09-doctor-command/E09-Audit.md +207 -0
- package/.savepoint/releases/v1.1/epics/E09-doctor-command/E09-Detail.md +65 -0
- package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T001-cli-entrypoint.md +24 -0
- package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T002-config-router-validation.md +28 -0
- package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T003-structure-checks.md +29 -0
- package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T004-dependency-checks.md +27 -0
- package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T005-audit-orphan-checks.md +28 -0
- package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T006-quality-gates-report.md +31 -0
- package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/E11-Detail.md +36 -0
- package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T001-debug-logging.md +25 -0
- package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T002-increase-debounce.md +21 -0
- package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T003-error-handling.md +22 -0
- package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T004-test-verify.md +29 -0
- package/.savepoint/releases/v1.1/epics/E12-validation-fix/E12-Audit.md +444 -0
- package/.savepoint/releases/v1.1/epics/E12-validation-fix/E12-Detail.md +45 -0
- package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T001-default-phase.md +35 -0
- package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T002-default-status.md +19 -0
- package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T003-better-errors.md +29 -0
- package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T004-validate-on-write.md +25 -0
- package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T005-tests.md +37 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/E13-Audit.md +118 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/E13-Detail.md +73 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T001-safe-cleanup.md +66 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T002-bug-fixes.md +35 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T003-centralize-duplication.md +60 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T004-infrastructure.md +33 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T005-decompose-update.md +37 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T006-async-io.md +40 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T007-test-coverage.md +37 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/E14-Audit.md +267 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/E14-Detail.md +54 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T001-group-model.md +39 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T002-data-interfaces.md +42 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T003-discover-orphans.md +33 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T004-epic-panel-headings.md +35 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T005-shell-tokenization.md +27 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T006-unify-enums.md +29 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T007-testutil-package.md +28 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/E15-Audit.md +272 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/E15-Detail.md +60 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T001-benchmarks.md +31 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T002-fuzz-targets.md +34 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T003-debug-flag.md +30 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T004-dist-checksums.md +27 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T005-windows-targets.md +28 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T006-abbreviation-splitting.md +26 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T007-root-test-allowlist.md +33 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T008-ci-and-release-automation.md +46 -0
- package/.savepoint/releases/v1.1/epics/_archived/T001-cli-entrypoint.md +25 -0
- package/.savepoint/releases/v1.1/epics/_archived/T002-quality-gates.md +27 -0
- package/.savepoint/releases/v1.1/epics/_archived/T003-snapshot.md +27 -0
- package/.savepoint/releases/v1.1/epics/_archived/T004-ai-reconcile.md +29 -0
- package/.savepoint/releases/v1.1/epics/_archived/T006-tui-review.md +31 -0
- package/.savepoint/releases/v1.1/epics/_archived/T008-skip-handling.md +34 -0
- package/.savepoint/releases/v1.1/v1.1-PRD.md +67 -7
- package/.savepoint/router.md +10 -17
- package/AGENTS.md +39 -24
- package/Makefile +3 -1
- package/README.md +0 -1
- package/agent-skills/savepoint-audit/SKILL.md +86 -34
- package/agent-skills/savepoint-build-task/SKILL.md +7 -2
- package/agent-skills/savepoint-create-plan/SKILL.md +7 -2
- package/agent-skills/savepoint-create-task/SKILL.md +44 -31
- package/agent-skills/savepoint-draft-prd/SKILL.md +7 -2
- package/agent-skills/savepoint-system-design/SKILL.md +7 -2
- package/agent_skills_test.go +91 -0
- package/cmd/board.go +59 -0
- package/cmd/board_test.go +137 -0
- package/cmd/doctor.go +53 -0
- package/cmd/doctor_test.go +146 -0
- package/cmd/init.go +63 -0
- package/cmd/init_test.go +104 -0
- package/internal/board/board.go +44 -36
- package/internal/board/board_test.go +27 -82
- package/internal/board/card.go +43 -23
- package/internal/board/card_test.go +74 -5
- package/internal/board/column.go +75 -15
- package/internal/board/column_test.go +76 -2
- package/internal/board/debug.go +26 -0
- package/internal/board/debug_test.go +108 -0
- package/internal/board/detail.go +33 -47
- package/internal/board/detail_test.go +48 -0
- package/internal/board/epic_panel.go +120 -22
- package/internal/board/epic_panel_test.go +302 -17
- package/internal/board/help.go +1 -0
- package/internal/board/help_test.go +1 -0
- package/internal/board/integration_test.go +266 -0
- package/internal/board/interfaces.go +65 -0
- package/internal/board/interfaces_test.go +114 -0
- package/internal/board/io.go +93 -0
- package/internal/board/model.go +79 -118
- package/internal/board/plain.go +88 -0
- package/internal/board/plain_test.go +117 -0
- package/internal/board/release.go +1 -9
- package/internal/board/release_test.go +6 -6
- package/internal/board/status.go +4 -4
- package/internal/board/theme.go +24 -0
- package/internal/board/theme_test.go +31 -0
- package/internal/board/transitions.go +113 -88
- package/internal/board/transitions_test.go +164 -141
- package/internal/board/tui.go +32 -0
- package/internal/board/update.go +344 -215
- package/internal/board/update_test.go +326 -18
- package/internal/board/util.go +76 -0
- package/internal/board/view.go +31 -28
- package/internal/board/view_test.go +74 -2
- package/internal/board/watch.go +41 -5
- package/internal/buildtool/main.go +45 -15
- package/internal/buildtool/main_test.go +224 -0
- package/internal/data/config.go +17 -3
- package/internal/data/config_test.go +49 -0
- package/internal/data/discover.go +26 -0
- package/internal/data/discover_test.go +34 -10
- package/internal/data/errors.go +4 -0
- package/internal/data/fuzz_test.go +75 -0
- package/internal/data/lifecycle.go +13 -6
- package/internal/data/lifecycle_test.go +14 -11
- package/internal/data/parser.go +22 -6
- package/internal/data/parser_test.go +31 -7
- package/internal/data/task.go +0 -9
- package/internal/data/testdata/fuzz/FuzzSplitFrontmatterBody/68eb66b0fe91e7e3 +2 -0
- package/internal/data/write.go +88 -11
- package/internal/data/write_test.go +167 -0
- package/internal/doctor/checks.go +567 -0
- package/internal/doctor/checks_test.go +716 -0
- package/internal/doctor/gates.go +193 -0
- package/internal/doctor/gates_test.go +166 -0
- package/internal/doctor/interfaces.go +64 -0
- package/internal/doctor/interfaces_test.go +104 -0
- package/internal/doctor/repairs.go +80 -0
- package/internal/doctor/repairs_test.go +81 -0
- package/internal/doctor/report.go +157 -0
- package/internal/doctor/report_test.go +89 -0
- package/internal/init/clipboard.go +146 -0
- package/internal/init/clipboard_test.go +74 -0
- package/internal/init/install.go +16 -0
- package/internal/init/integration_test.go +197 -0
- package/internal/init/prompt.go +14 -0
- package/internal/init/prompt_test.go +77 -0
- package/internal/init/scaffold.go +59 -0
- package/internal/init/scaffold_test.go +179 -0
- package/internal/init/template_freshness_test.go +56 -0
- package/internal/init/validate.go +85 -0
- package/internal/init/validate_test.go +141 -0
- package/internal/init/write.go +73 -0
- package/internal/init/write_test.go +91 -0
- package/internal/styles/styles_test.go +133 -0
- package/internal/testutil/fixture.go +113 -0
- package/internal/testutil/fs.go +26 -0
- package/main.go +120 -4
- package/package.json +2 -2
- package/project-audit/audit_report_glm_5.1.md +411 -0
- package/project-audit/audit_report_opus_4.6.md +406 -0
- package/project-audit/consolidated-audit-report.md +456 -0
- package/templates/project/.savepoint/Design.md +2 -2
- package/templates/project/.savepoint/router.md +10 -10
- package/templates/project/AGENTS.md +33 -21
- package/templates/project/agent-skills/savepoint-audit/SKILL.md +87 -0
- package/templates/project/agent-skills/savepoint-build-task/SKILL.md +44 -0
- package/templates/project/agent-skills/savepoint-create-plan/SKILL.md +33 -0
- package/templates/project/agent-skills/savepoint-create-task/SKILL.md +44 -0
- package/templates/project/agent-skills/savepoint-draft-prd/SKILL.md +37 -0
- package/templates/project/agent-skills/savepoint-system-design/SKILL.md +38 -0
- package/templates/prompts/audit-reconciliation.prompt.md +33 -28
- package/templates/prompts/design.prompt.md +3 -1
- package/.savepoint/audit/v1/E01/proposals.md +0 -168
- package/.savepoint/audit/v1/E01/snapshot.md +0 -78
- package/.savepoint/audit/v1/E01-go-setup/proposals.md +0 -166
- package/.savepoint/audit/v1/E01-go-setup/snapshot.md +0 -71
- package/.savepoint/audit/v1/E01-scaffolding/proposals/AGENTS.md +0 -66
- package/.savepoint/audit/v1/E01-scaffolding/proposals/Design.md +0 -210
- package/.savepoint/audit/v1/E01-scaffolding/proposals/epic-Design.md +0 -117
- package/.savepoint/audit/v1/E01-scaffolding/proposals/quality-review.md +0 -101
- package/.savepoint/audit/v1/E01-scaffolding/snapshot.md +0 -54
- package/.savepoint/audit/v1/E02-data-model/snapshot.md +0 -128
- package/.savepoint/audit/v1/E02-data-readers/proposals.md +0 -123
- package/.savepoint/audit/v1/E02-data-readers/snapshot.md +0 -54
- package/.savepoint/audit/v1/E03-board-tui-core/proposals.md +0 -146
- package/.savepoint/audit/v1/E03-board-tui-core/snapshot.md +0 -57
- package/.savepoint/audit/v1/E03-cli-foundation/snapshot.md +0 -106
- package/.savepoint/audit/v1/E04-board-components/proposals.md +0 -118
- package/.savepoint/audit/v1/E04-board-components/snapshot.md +0 -77
- package/.savepoint/audit/v1/E04-templates-and-prompts/snapshot.md +0 -115
- package/.savepoint/audit/v1/E05-init-command/snapshot.md +0 -125
- package/.savepoint/audit/v1/E05-phase-transitions/proposals.md +0 -83
- package/.savepoint/audit/v1/E05-phase-transitions/snapshot.md +0 -36
- package/.savepoint/audit/v1/E06-atari-noir-layout/proposals.md +0 -130
- package/.savepoint/audit/v1/E06-atari-noir-layout/snapshot.md +0 -84
- package/.savepoint/audit/v1/E06-tui-board/snapshot.md +0 -64
- package/.savepoint/audit/v1/E07-audit-pipeline/snapshot.md +0 -165
- package/.savepoint/audit/v1/E08-board-workflow-cleanup/snapshot.md +0 -65
- package/.savepoint/audit/v1.1/E02-cross-platform-compatibility/snapshot.md +0 -41
- package/.savepoint/audit/v1.1/E04-epic-navigation/snapshot.md +0 -48
- package/ink-cli-ui-design.zip +0 -0
- package/savepoint +0 -0
- package/savepoint.exe +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package board
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"fmt"
|
|
4
5
|
"strings"
|
|
5
6
|
"testing"
|
|
6
7
|
|
|
@@ -66,7 +67,7 @@ func TestView_containsFooterHints(t *testing.T) {
|
|
|
66
67
|
m := NewModel(nil, "v1", "E03")
|
|
67
68
|
footer := m.renderFooter(80)
|
|
68
69
|
|
|
69
|
-
if !strings.Contains(footer, "←/→:nav
|
|
70
|
+
if !strings.Contains(footer, "←/→:nav p: Priority R:release ?:help q:quit") {
|
|
70
71
|
t.Fatal("renderFooter() missing navigation hints")
|
|
71
72
|
}
|
|
72
73
|
|
|
@@ -75,7 +76,7 @@ func TestView_containsFooterHints(t *testing.T) {
|
|
|
75
76
|
t.Fatalf("renderFooter() returned %d lines, want 3", len(lines))
|
|
76
77
|
}
|
|
77
78
|
if strings.TrimSpace(plainTerminal(lines[1])) != "" {
|
|
78
|
-
t.Fatalf("renderFooter()
|
|
79
|
+
t.Fatalf("renderFooter() status line = %q, want blank", lines[1])
|
|
79
80
|
}
|
|
80
81
|
for i, line := range lines {
|
|
81
82
|
if got := lipgloss.Width(line); got > 80 {
|
|
@@ -84,6 +85,16 @@ func TestView_containsFooterHints(t *testing.T) {
|
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
func TestView_footerRendersStatusMessage(t *testing.T) {
|
|
89
|
+
m := NewModel(nil, "v1", "E03")
|
|
90
|
+
m.StatusMessage = "Router set to v1.1 E05-tasking-permissions/T004"
|
|
91
|
+
footer := plainTerminal(m.renderFooter(80))
|
|
92
|
+
|
|
93
|
+
if !strings.Contains(footer, "Router set to v1.1 E05-tasking-permissions/T004") {
|
|
94
|
+
t.Fatal("renderFooter() missing status message")
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
87
98
|
func TestView_containsBottomDivider(t *testing.T) {
|
|
88
99
|
m := NewModel(nil, "v1", "E03")
|
|
89
100
|
m.Width = 120
|
|
@@ -291,6 +302,67 @@ func TestRenderNextActivityLine_truncatesAtNarrowWidth(t *testing.T) {
|
|
|
291
302
|
}
|
|
292
303
|
}
|
|
293
304
|
|
|
305
|
+
func BenchmarkCalculateLayout_narrow(b *testing.B) {
|
|
306
|
+
b.ReportAllocs()
|
|
307
|
+
for b.Loop() {
|
|
308
|
+
CalculateLayout(60, 24)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
func BenchmarkCalculateLayout_standard(b *testing.B) {
|
|
313
|
+
b.ReportAllocs()
|
|
314
|
+
for b.Loop() {
|
|
315
|
+
CalculateLayout(80, 24)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
func BenchmarkCalculateLayout_wide(b *testing.B) {
|
|
320
|
+
b.ReportAllocs()
|
|
321
|
+
for b.Loop() {
|
|
322
|
+
CalculateLayout(120, 24)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
func BenchmarkCalculateLayout_extraWide(b *testing.B) {
|
|
327
|
+
b.ReportAllocs()
|
|
328
|
+
for b.Loop() {
|
|
329
|
+
CalculateLayout(220, 50)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
func BenchmarkView_empty(b *testing.B) {
|
|
334
|
+
m := NewModel(nil, "v1", "E03")
|
|
335
|
+
m.Width = 120
|
|
336
|
+
m.Height = 40
|
|
337
|
+
b.ReportAllocs()
|
|
338
|
+
for b.Loop() {
|
|
339
|
+
m.View()
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
func BenchmarkView_withTasks(b *testing.B) {
|
|
344
|
+
tasks := make([]data.Task, 15)
|
|
345
|
+
stages := []data.ProgressStage{data.StageBuild, data.StageTest, data.StageAudit}
|
|
346
|
+
cols := []data.ColumnType{data.ColumnPlanned, data.ColumnInProgress, data.ColumnDone}
|
|
347
|
+
for i := range tasks {
|
|
348
|
+
tasks[i] = data.Task{
|
|
349
|
+
ID: fmt.Sprintf("E06-layout/T%03d-task-slug", i+1),
|
|
350
|
+
Title: fmt.Sprintf("Task %d with a reasonable title length", i+1),
|
|
351
|
+
Column: cols[i%3],
|
|
352
|
+
Stage: stages[i%3],
|
|
353
|
+
Release: "v1.1",
|
|
354
|
+
Epic: "E06-layout",
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
m := NewModel(tasks, "v1.1", "E06")
|
|
358
|
+
m.Width = 120
|
|
359
|
+
m.Height = 40
|
|
360
|
+
b.ReportAllocs()
|
|
361
|
+
for b.Loop() {
|
|
362
|
+
m.View()
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
294
366
|
func TestView_narrowShowsSingleColumn(t *testing.T) {
|
|
295
367
|
m := NewModel(nil, "v1", "E03")
|
|
296
368
|
m.Width = 60
|
package/internal/board/watch.go
CHANGED
|
@@ -16,6 +16,31 @@ type reloadMsg struct {
|
|
|
16
16
|
releases []string
|
|
17
17
|
releaseEpics map[string][]string
|
|
18
18
|
epicStatuses map[string]string
|
|
19
|
+
routerState *data.RouterState
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type routerWriteMsg struct {
|
|
23
|
+
message string
|
|
24
|
+
state *data.RouterState
|
|
25
|
+
taskID string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type taskWriteMsg struct {
|
|
29
|
+
prefix string
|
|
30
|
+
next data.Task
|
|
31
|
+
err error
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type epicDetailMsg struct {
|
|
35
|
+
content string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type auditContentMsg struct {
|
|
39
|
+
content string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type errorMsg struct {
|
|
43
|
+
message string
|
|
19
44
|
}
|
|
20
45
|
|
|
21
46
|
// watchFiles blocks until a file event arrives, debounces for 100ms, emits fileChangeMsg.
|
|
@@ -27,6 +52,7 @@ func watchFiles(w *fsnotify.Watcher) tea.Cmd {
|
|
|
27
52
|
if !ok {
|
|
28
53
|
return nil
|
|
29
54
|
}
|
|
55
|
+
debugf("watcher: event %s", event)
|
|
30
56
|
watchCreatedDir(w, event)
|
|
31
57
|
timer := time.NewTimer(100 * time.Millisecond)
|
|
32
58
|
drain:
|
|
@@ -37,11 +63,13 @@ func watchFiles(w *fsnotify.Watcher) tea.Cmd {
|
|
|
37
63
|
timer.Stop()
|
|
38
64
|
return nil
|
|
39
65
|
}
|
|
66
|
+
debugf("watcher: event %s", event)
|
|
40
67
|
watchCreatedDir(w, event)
|
|
41
68
|
case <-timer.C:
|
|
42
69
|
break drain
|
|
43
70
|
}
|
|
44
71
|
}
|
|
72
|
+
debugf("watcher: emitting fileChangeMsg")
|
|
45
73
|
return fileChangeMsg{}
|
|
46
74
|
case _, ok := <-w.Errors:
|
|
47
75
|
if !ok {
|
|
@@ -52,22 +80,30 @@ func watchFiles(w *fsnotify.Watcher) tea.Cmd {
|
|
|
52
80
|
}
|
|
53
81
|
}
|
|
54
82
|
|
|
55
|
-
func reloadTasks(root string) tea.Cmd {
|
|
83
|
+
func reloadTasks(root string, deps ModelDependencies) tea.Cmd {
|
|
56
84
|
return func() tea.Msg {
|
|
57
|
-
|
|
85
|
+
debugf("reload: starting task reload from %q", root)
|
|
86
|
+
tasks, releases, releaseEpics, epicStatuses, err := loadBoardData(root, deps.Discoverer, deps.Parser)
|
|
58
87
|
if err != nil {
|
|
59
|
-
|
|
88
|
+
debugf("reload: error: %v", err)
|
|
89
|
+
return errorMsg{message: "reload failed: " + err.Error()}
|
|
60
90
|
}
|
|
61
|
-
|
|
91
|
+
debugf("reload: loaded %d tasks", len(tasks))
|
|
92
|
+
routerState, _ := readRouterState(root, deps.RouterReader)
|
|
93
|
+
return reloadMsg{tasks: tasks, releases: releases, releaseEpics: releaseEpics, epicStatuses: epicStatuses, routerState: routerState}
|
|
62
94
|
}
|
|
63
95
|
}
|
|
64
96
|
|
|
65
|
-
// newWatcher watches the
|
|
97
|
+
// newWatcher watches the savepoint root (for router.md) and all releases subdirs.
|
|
66
98
|
func newWatcher(root string) (*fsnotify.Watcher, error) {
|
|
67
99
|
w, err := fsnotify.NewWatcher()
|
|
68
100
|
if err != nil {
|
|
69
101
|
return nil, err
|
|
70
102
|
}
|
|
103
|
+
if err := w.Add(root); err != nil {
|
|
104
|
+
w.Close()
|
|
105
|
+
return nil, err
|
|
106
|
+
}
|
|
71
107
|
releasesPath := filepath.Join(root, "releases")
|
|
72
108
|
if err := addDirsRecursive(w, releasesPath); err != nil {
|
|
73
109
|
w.Close()
|
|
@@ -3,6 +3,8 @@ package main
|
|
|
3
3
|
import (
|
|
4
4
|
"archive/tar"
|
|
5
5
|
"compress/gzip"
|
|
6
|
+
"crypto/sha256"
|
|
7
|
+
"encoding/hex"
|
|
6
8
|
"errors"
|
|
7
9
|
"flag"
|
|
8
10
|
"fmt"
|
|
@@ -11,6 +13,7 @@ import (
|
|
|
11
13
|
"os/exec"
|
|
12
14
|
"path/filepath"
|
|
13
15
|
"runtime"
|
|
16
|
+
"strings"
|
|
14
17
|
)
|
|
15
18
|
|
|
16
19
|
type target struct {
|
|
@@ -23,6 +26,8 @@ var targets = []target{
|
|
|
23
26
|
{os: "linux", arch: "arm64"},
|
|
24
27
|
{os: "darwin", arch: "amd64"},
|
|
25
28
|
{os: "darwin", arch: "arm64"},
|
|
29
|
+
{os: "windows", arch: "amd64"},
|
|
30
|
+
{os: "windows", arch: "arm64"},
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
var versionOverride string
|
|
@@ -41,7 +46,7 @@ func run(args []string) error {
|
|
|
41
46
|
return err
|
|
42
47
|
}
|
|
43
48
|
if flags.NArg() != 1 {
|
|
44
|
-
return errors.New("usage: go run ./internal/buildtool [-version vX.Y.Z] <build|clean|build-linux|build-darwin|build-all|dist|smoke-test>")
|
|
49
|
+
return errors.New("usage: go run ./internal/buildtool [-version vX.Y.Z] <build|clean|build-linux|build-darwin|build-windows|build-all|dist|smoke-test>")
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
switch flags.Arg(0) {
|
|
@@ -53,6 +58,8 @@ func run(args []string) error {
|
|
|
53
58
|
return buildMatching("linux")
|
|
54
59
|
case "build-darwin":
|
|
55
60
|
return buildMatching("darwin")
|
|
61
|
+
case "build-windows":
|
|
62
|
+
return buildMatching("windows")
|
|
56
63
|
case "build-all":
|
|
57
64
|
return buildAll()
|
|
58
65
|
case "dist":
|
|
@@ -98,8 +105,15 @@ func buildAll() error {
|
|
|
98
105
|
return nil
|
|
99
106
|
}
|
|
100
107
|
|
|
108
|
+
func executableName(goos string) string {
|
|
109
|
+
if goos == "windows" {
|
|
110
|
+
return "savepoint.exe"
|
|
111
|
+
}
|
|
112
|
+
return "savepoint"
|
|
113
|
+
}
|
|
114
|
+
|
|
101
115
|
func buildTarget(target target) error {
|
|
102
|
-
output := filepath.Join("dist", target.os+"-"+target.arch,
|
|
116
|
+
output := filepath.Join("dist", target.os+"-"+target.arch, executableName(target.os))
|
|
103
117
|
return runGoBuild(output, target.os, target.arch)
|
|
104
118
|
}
|
|
105
119
|
|
|
@@ -122,13 +136,39 @@ func dist() error {
|
|
|
122
136
|
if err := buildAll(); err != nil {
|
|
123
137
|
return err
|
|
124
138
|
}
|
|
139
|
+
var archives []string
|
|
125
140
|
for _, target := range targets {
|
|
126
141
|
name := "savepoint-" + version() + "-" + target.os + "-" + target.arch + ".tar.gz"
|
|
127
|
-
source := filepath.Join("dist", target.os+"-"+target.arch,
|
|
142
|
+
source := filepath.Join("dist", target.os+"-"+target.arch, executableName(target.os))
|
|
128
143
|
archive := filepath.Join("dist", name)
|
|
129
|
-
if err := writeTarGz(archive, source,
|
|
144
|
+
if err := writeTarGz(archive, source, executableName(target.os)); err != nil {
|
|
130
145
|
return err
|
|
131
146
|
}
|
|
147
|
+
archives = append(archives, archive)
|
|
148
|
+
}
|
|
149
|
+
return writeChecksums(filepath.Join("dist", "checksums.txt"), archives)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
func writeChecksums(dest string, archives []string) error {
|
|
153
|
+
var lines strings.Builder
|
|
154
|
+
for _, path := range archives {
|
|
155
|
+
f, err := os.Open(path)
|
|
156
|
+
if err != nil {
|
|
157
|
+
return fmt.Errorf("checksum open %s: %w", path, err)
|
|
158
|
+
}
|
|
159
|
+
h := sha256.New()
|
|
160
|
+
if _, err := io.Copy(h, f); err != nil {
|
|
161
|
+
f.Close()
|
|
162
|
+
return fmt.Errorf("checksum read %s: %w", path, err)
|
|
163
|
+
}
|
|
164
|
+
f.Close()
|
|
165
|
+
lines.WriteString(hex.EncodeToString(h.Sum(nil)))
|
|
166
|
+
lines.WriteString(" ")
|
|
167
|
+
lines.WriteString(filepath.Base(path))
|
|
168
|
+
lines.WriteString("\n")
|
|
169
|
+
}
|
|
170
|
+
if err := os.WriteFile(dest, []byte(lines.String()), 0o644); err != nil {
|
|
171
|
+
return fmt.Errorf("write checksums: %w", err)
|
|
132
172
|
}
|
|
133
173
|
return nil
|
|
134
174
|
}
|
|
@@ -196,7 +236,7 @@ func version() string {
|
|
|
196
236
|
cmd := exec.Command("git", "describe", "--tags", "--abbrev=0")
|
|
197
237
|
output, err := cmd.Output()
|
|
198
238
|
if err == nil && len(output) > 0 {
|
|
199
|
-
return string(
|
|
239
|
+
return strings.TrimSpace(string(output))
|
|
200
240
|
}
|
|
201
241
|
return "v0.0.0"
|
|
202
242
|
}
|
|
@@ -207,13 +247,3 @@ func localExecutable() string {
|
|
|
207
247
|
}
|
|
208
248
|
return "savepoint"
|
|
209
249
|
}
|
|
210
|
-
|
|
211
|
-
func trimSpace(value []byte) []byte {
|
|
212
|
-
for len(value) > 0 && (value[len(value)-1] == '\n' || value[len(value)-1] == '\r' || value[len(value)-1] == '\t' || value[len(value)-1] == ' ') {
|
|
213
|
-
value = value[:len(value)-1]
|
|
214
|
-
}
|
|
215
|
-
for len(value) > 0 && (value[0] == '\n' || value[0] == '\r' || value[0] == '\t' || value[0] == ' ') {
|
|
216
|
-
value = value[1:]
|
|
217
|
-
}
|
|
218
|
-
return value
|
|
219
|
-
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"archive/tar"
|
|
5
|
+
"compress/gzip"
|
|
6
|
+
"crypto/sha256"
|
|
7
|
+
"encoding/hex"
|
|
8
|
+
"io"
|
|
9
|
+
"os"
|
|
10
|
+
"path/filepath"
|
|
11
|
+
"runtime"
|
|
12
|
+
"strings"
|
|
13
|
+
"testing"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
func TestVersion_override(t *testing.T) {
|
|
17
|
+
versionOverride = "v1.2.3"
|
|
18
|
+
defer func() { versionOverride = "" }()
|
|
19
|
+
if got := version(); got != "v1.2.3" {
|
|
20
|
+
t.Errorf("version() = %q, want %q", got, "v1.2.3")
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func TestVersion_env(t *testing.T) {
|
|
25
|
+
versionOverride = ""
|
|
26
|
+
os.Setenv("VERSION", "v2.0.0-env")
|
|
27
|
+
defer os.Unsetenv("VERSION")
|
|
28
|
+
if got := version(); got != "v2.0.0-env" {
|
|
29
|
+
t.Errorf("version() = %q, want %q", got, "v2.0.0-env")
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func TestVersion_fallback(t *testing.T) {
|
|
34
|
+
versionOverride = ""
|
|
35
|
+
os.Unsetenv("VERSION")
|
|
36
|
+
got := version()
|
|
37
|
+
if got == "" {
|
|
38
|
+
t.Error("version() returned empty string")
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func TestWriteChecksums(t *testing.T) {
|
|
43
|
+
dir := t.TempDir()
|
|
44
|
+
|
|
45
|
+
content := []byte("fake archive content")
|
|
46
|
+
archive := filepath.Join(dir, "savepoint-v1.0.0-linux-amd64.tar.gz")
|
|
47
|
+
if err := os.WriteFile(archive, content, 0o644); err != nil {
|
|
48
|
+
t.Fatal(err)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
dest := filepath.Join(dir, "checksums.txt")
|
|
52
|
+
if err := writeChecksums(dest, []string{archive}); err != nil {
|
|
53
|
+
t.Fatalf("writeChecksums: %v", err)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
got, err := os.ReadFile(dest)
|
|
57
|
+
if err != nil {
|
|
58
|
+
t.Fatal(err)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
h := sha256.Sum256(content)
|
|
62
|
+
wantHash := hex.EncodeToString(h[:])
|
|
63
|
+
wantLine := wantHash + " savepoint-v1.0.0-linux-amd64.tar.gz"
|
|
64
|
+
|
|
65
|
+
lines := strings.Split(strings.TrimSpace(string(got)), "\n")
|
|
66
|
+
if len(lines) != 1 {
|
|
67
|
+
t.Fatalf("expected 1 line, got %d", len(lines))
|
|
68
|
+
}
|
|
69
|
+
if lines[0] != wantLine {
|
|
70
|
+
t.Errorf("line = %q, want %q", lines[0], wantLine)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
func TestWriteChecksums_multiple(t *testing.T) {
|
|
75
|
+
dir := t.TempDir()
|
|
76
|
+
|
|
77
|
+
names := []string{"a.tar.gz", "b.tar.gz"}
|
|
78
|
+
var paths []string
|
|
79
|
+
for _, name := range names {
|
|
80
|
+
p := filepath.Join(dir, name)
|
|
81
|
+
if err := os.WriteFile(p, []byte(name), 0o644); err != nil {
|
|
82
|
+
t.Fatal(err)
|
|
83
|
+
}
|
|
84
|
+
paths = append(paths, p)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
dest := filepath.Join(dir, "checksums.txt")
|
|
88
|
+
if err := writeChecksums(dest, paths); err != nil {
|
|
89
|
+
t.Fatalf("writeChecksums: %v", err)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
got, err := os.ReadFile(dest)
|
|
93
|
+
if err != nil {
|
|
94
|
+
t.Fatal(err)
|
|
95
|
+
}
|
|
96
|
+
lines := strings.Split(strings.TrimSpace(string(got)), "\n")
|
|
97
|
+
if len(lines) != 2 {
|
|
98
|
+
t.Fatalf("expected 2 lines, got %d: %s", len(lines), got)
|
|
99
|
+
}
|
|
100
|
+
for i, name := range names {
|
|
101
|
+
h := sha256.Sum256([]byte(name))
|
|
102
|
+
want := hex.EncodeToString(h[:]) + " " + name
|
|
103
|
+
if lines[i] != want {
|
|
104
|
+
t.Errorf("line[%d] = %q, want %q", i, lines[i], want)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func TestWriteChecksums_missingFile(t *testing.T) {
|
|
110
|
+
dir := t.TempDir()
|
|
111
|
+
dest := filepath.Join(dir, "checksums.txt")
|
|
112
|
+
err := writeChecksums(dest, []string{filepath.Join(dir, "nonexistent.tar.gz")})
|
|
113
|
+
if err == nil {
|
|
114
|
+
t.Error("expected error for missing archive, got nil")
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
func TestTargets_includesWindows(t *testing.T) {
|
|
119
|
+
var gotAMD64, gotARM64 bool
|
|
120
|
+
for _, tgt := range targets {
|
|
121
|
+
if tgt.os != "windows" {
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
switch tgt.arch {
|
|
125
|
+
case "amd64":
|
|
126
|
+
gotAMD64 = true
|
|
127
|
+
case "arm64":
|
|
128
|
+
gotARM64 = true
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if !gotAMD64 {
|
|
132
|
+
t.Error("targets missing windows/amd64")
|
|
133
|
+
}
|
|
134
|
+
if !gotARM64 {
|
|
135
|
+
t.Error("targets missing windows/arm64")
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func TestTargets_preservesLinuxDarwin(t *testing.T) {
|
|
140
|
+
want := map[string]bool{
|
|
141
|
+
"linux/amd64": false,
|
|
142
|
+
"linux/arm64": false,
|
|
143
|
+
"darwin/amd64": false,
|
|
144
|
+
"darwin/arm64": false,
|
|
145
|
+
}
|
|
146
|
+
for _, tgt := range targets {
|
|
147
|
+
key := tgt.os + "/" + tgt.arch
|
|
148
|
+
if _, ok := want[key]; ok {
|
|
149
|
+
want[key] = true
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for key, found := range want {
|
|
153
|
+
if !found {
|
|
154
|
+
t.Errorf("targets missing %s", key)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
func TestExecutableName(t *testing.T) {
|
|
160
|
+
if got := executableName("windows"); got != "savepoint.exe" {
|
|
161
|
+
t.Errorf("executableName(windows) = %q, want savepoint.exe", got)
|
|
162
|
+
}
|
|
163
|
+
if got := executableName("linux"); got != "savepoint" {
|
|
164
|
+
t.Errorf("executableName(linux) = %q, want savepoint", got)
|
|
165
|
+
}
|
|
166
|
+
if got := executableName("darwin"); got != "savepoint" {
|
|
167
|
+
t.Errorf("executableName(darwin) = %q, want savepoint", got)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
func TestWriteTarGzPreservesWindowsExecutableName(t *testing.T) {
|
|
172
|
+
dir := t.TempDir()
|
|
173
|
+
source := filepath.Join(dir, "savepoint.exe")
|
|
174
|
+
if err := os.WriteFile(source, []byte("binary"), 0o755); err != nil {
|
|
175
|
+
t.Fatal(err)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
archive := filepath.Join(dir, "savepoint-windows-amd64.tar.gz")
|
|
179
|
+
if err := writeTarGz(archive, source, executableName("windows")); err != nil {
|
|
180
|
+
t.Fatalf("writeTarGz: %v", err)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
f, err := os.Open(archive)
|
|
184
|
+
if err != nil {
|
|
185
|
+
t.Fatal(err)
|
|
186
|
+
}
|
|
187
|
+
defer f.Close()
|
|
188
|
+
|
|
189
|
+
gz, err := gzip.NewReader(f)
|
|
190
|
+
if err != nil {
|
|
191
|
+
t.Fatal(err)
|
|
192
|
+
}
|
|
193
|
+
defer gz.Close()
|
|
194
|
+
|
|
195
|
+
tr := tar.NewReader(gz)
|
|
196
|
+
header, err := tr.Next()
|
|
197
|
+
if err != nil {
|
|
198
|
+
t.Fatal(err)
|
|
199
|
+
}
|
|
200
|
+
if header.Name != "savepoint.exe" {
|
|
201
|
+
t.Fatalf("archive member = %q, want savepoint.exe", header.Name)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
content, err := io.ReadAll(tr)
|
|
205
|
+
if err != nil {
|
|
206
|
+
t.Fatal(err)
|
|
207
|
+
}
|
|
208
|
+
if string(content) != "binary" {
|
|
209
|
+
t.Fatalf("archive content = %q, want binary", content)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
func TestLocalExecutable(t *testing.T) {
|
|
214
|
+
got := localExecutable()
|
|
215
|
+
if runtime.GOOS == "windows" {
|
|
216
|
+
if got != "savepoint.exe" {
|
|
217
|
+
t.Errorf("localExecutable() = %q, want %q", got, "savepoint.exe")
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
if got != "savepoint" {
|
|
221
|
+
t.Errorf("localExecutable() = %q, want %q", got, "savepoint")
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
package/internal/data/config.go
CHANGED
|
@@ -16,8 +16,17 @@ type Theme struct {
|
|
|
16
16
|
Accents map[string]string `yaml:"accents"`
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
type QualityGates struct {
|
|
20
|
+
Lint *string `yaml:"lint"`
|
|
21
|
+
Typecheck *string `yaml:"typecheck"`
|
|
22
|
+
Test *string `yaml:"test"`
|
|
23
|
+
BlockOnFailure bool `yaml:"block_on_failure"`
|
|
24
|
+
Timeout string `yaml:"gate_timeout"`
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
type Config struct {
|
|
20
|
-
Theme
|
|
28
|
+
Theme Theme `yaml:"theme"`
|
|
29
|
+
QualityGates QualityGates `yaml:"quality_gates"`
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
var defaultTheme = Theme{
|
|
@@ -80,8 +89,13 @@ func fillThemeDefaults(theme Theme) Theme {
|
|
|
80
89
|
if theme.Text == "" {
|
|
81
90
|
theme.Text = defaultTheme.Text
|
|
82
91
|
}
|
|
83
|
-
if
|
|
84
|
-
theme.Accents =
|
|
92
|
+
if theme.Accents == nil {
|
|
93
|
+
theme.Accents = make(map[string]string)
|
|
94
|
+
}
|
|
95
|
+
for k, v := range defaultTheme.Accents {
|
|
96
|
+
if _, ok := theme.Accents[k]; !ok {
|
|
97
|
+
theme.Accents[k] = v
|
|
98
|
+
}
|
|
85
99
|
}
|
|
86
100
|
return theme
|
|
87
101
|
}
|
|
@@ -53,6 +53,55 @@ func TestConfigReaderRead(t *testing.T) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
func TestFillThemeDefaults_PartialAccents(t *testing.T) {
|
|
57
|
+
theme := Theme{
|
|
58
|
+
BG: "#000000",
|
|
59
|
+
Accents: map[string]string{"planned": "#ff0000"},
|
|
60
|
+
}
|
|
61
|
+
result := fillThemeDefaults(theme)
|
|
62
|
+
if result.Accents["planned"] != "#ff0000" {
|
|
63
|
+
t.Errorf("Accents[planned] = %v, want #ff0000 (user value preserved)", result.Accents["planned"])
|
|
64
|
+
}
|
|
65
|
+
if result.Accents["in_progress"] != defaultTheme.Accents["in_progress"] {
|
|
66
|
+
t.Errorf("Accents[in_progress] = %v, want default %v", result.Accents["in_progress"], defaultTheme.Accents["in_progress"])
|
|
67
|
+
}
|
|
68
|
+
if result.Accents["done"] != defaultTheme.Accents["done"] {
|
|
69
|
+
t.Errorf("Accents[done] = %v, want default %v", result.Accents["done"], defaultTheme.Accents["done"])
|
|
70
|
+
}
|
|
71
|
+
if result.Accents["blocked"] != defaultTheme.Accents["blocked"] {
|
|
72
|
+
t.Errorf("Accents[blocked] = %v, want default %v", result.Accents["blocked"], defaultTheme.Accents["blocked"])
|
|
73
|
+
}
|
|
74
|
+
if result.Accents["epic"] != defaultTheme.Accents["epic"] {
|
|
75
|
+
t.Errorf("Accents[epic] = %v, want default %v", result.Accents["epic"], defaultTheme.Accents["epic"])
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func TestFillThemeDefaults_NilAccents(t *testing.T) {
|
|
80
|
+
theme := Theme{
|
|
81
|
+
BG: "#000000",
|
|
82
|
+
Accents: nil,
|
|
83
|
+
}
|
|
84
|
+
result := fillThemeDefaults(theme)
|
|
85
|
+
for k, v := range defaultTheme.Accents {
|
|
86
|
+
if result.Accents[k] != v {
|
|
87
|
+
t.Errorf("Accents[%s] = %v, want default %v", k, result.Accents[k], v)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func TestFillThemeDefaults_EmptyAccents(t *testing.T) {
|
|
93
|
+
theme := Theme{
|
|
94
|
+
BG: "#000000",
|
|
95
|
+
Accents: map[string]string{},
|
|
96
|
+
}
|
|
97
|
+
result := fillThemeDefaults(theme)
|
|
98
|
+
for k, v := range defaultTheme.Accents {
|
|
99
|
+
if result.Accents[k] != v {
|
|
100
|
+
t.Errorf("Accents[%s] = %v, want default %v", k, result.Accents[k], v)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
56
105
|
func TestConfigReaderMalformedYAML(t *testing.T) {
|
|
57
106
|
tmpfile, err := os.CreateTemp("", "config-*.yml")
|
|
58
107
|
if err != nil {
|
|
@@ -85,6 +85,32 @@ func (d *Discover) ListReleases(root string) ([]ReleaseInfo, error) {
|
|
|
85
85
|
return releases, nil
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// ListRootDirs returns sorted child directory names directly under root.
|
|
89
|
+
func (d *Discover) ListRootDirs(root string) ([]string, error) {
|
|
90
|
+
info, err := os.Stat(root)
|
|
91
|
+
if err != nil {
|
|
92
|
+
return nil, err
|
|
93
|
+
}
|
|
94
|
+
if !info.IsDir() {
|
|
95
|
+
return nil, fmt.Errorf("%s is not a directory", root)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
entries, err := os.ReadDir(root)
|
|
99
|
+
if err != nil {
|
|
100
|
+
return nil, err
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
var dirs []string
|
|
104
|
+
for _, entry := range entries {
|
|
105
|
+
if entry.IsDir() {
|
|
106
|
+
dirs = append(dirs, entry.Name())
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
sort.Strings(dirs)
|
|
111
|
+
return dirs, nil
|
|
112
|
+
}
|
|
113
|
+
|
|
88
114
|
func (d *Discover) ListEpics(root, release string) ([]EpicInfo, error) {
|
|
89
115
|
epicsPath := filepath.Join(root, "releases", release, "epics")
|
|
90
116
|
info, err := os.Stat(epicsPath)
|