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
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
package init
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
|
|
9
|
+
"github.com/opencode/savepoint/internal/testutil"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
func TestAtomicWrite_createsFile(t *testing.T) {
|
|
13
|
+
dir := t.TempDir()
|
|
14
|
+
target := filepath.Join(dir, "output.txt")
|
|
15
|
+
|
|
16
|
+
if err := AtomicWrite(target, []byte("hello")); err != nil {
|
|
17
|
+
t.Fatalf("AtomicWrite() error = %v", err)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
data, err := os.ReadFile(target)
|
|
21
|
+
if err != nil {
|
|
22
|
+
t.Fatal(err)
|
|
23
|
+
}
|
|
24
|
+
if string(data) != "hello" {
|
|
25
|
+
t.Fatalf("got %q, want %q", string(data), "hello")
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func TestAtomicWrite_replacesExistingFile(t *testing.T) {
|
|
30
|
+
dir := t.TempDir()
|
|
31
|
+
target := filepath.Join(dir, "output.txt")
|
|
32
|
+
testutil.WriteFile(t, target, "old")
|
|
33
|
+
|
|
34
|
+
if err := AtomicWrite(target, []byte("new")); err != nil {
|
|
35
|
+
t.Fatalf("AtomicWrite() error = %v", err)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
data, err := os.ReadFile(target)
|
|
39
|
+
if err != nil {
|
|
40
|
+
t.Fatal(err)
|
|
41
|
+
}
|
|
42
|
+
if string(data) != "new" {
|
|
43
|
+
t.Fatalf("got %q, want %q", string(data), "new")
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
assertNoWriteArtifacts(t, dir)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func TestAtomicWrite_noTempFileLeftBehind(t *testing.T) {
|
|
50
|
+
dir := t.TempDir()
|
|
51
|
+
target := filepath.Join(dir, "output.txt")
|
|
52
|
+
|
|
53
|
+
if err := AtomicWrite(target, []byte("data")); err != nil {
|
|
54
|
+
t.Fatalf("AtomicWrite() error = %v", err)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
assertNoWriteArtifacts(t, dir)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func TestAtomicWrite_handlesNestedDirectories(t *testing.T) {
|
|
61
|
+
dir := t.TempDir()
|
|
62
|
+
target := filepath.Join(dir, "deep", "nested", "output.txt")
|
|
63
|
+
|
|
64
|
+
// Parent directories must exist before calling AtomicWrite
|
|
65
|
+
testutil.MkdirAll(t, filepath.Dir(target))
|
|
66
|
+
|
|
67
|
+
if err := AtomicWrite(target, []byte("nested")); err != nil {
|
|
68
|
+
t.Fatalf("AtomicWrite() error = %v", err)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if _, err := os.Stat(target); err != nil {
|
|
72
|
+
t.Errorf("target not created: %v", err)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func assertNoWriteArtifacts(t *testing.T, dir string) {
|
|
77
|
+
t.Helper()
|
|
78
|
+
|
|
79
|
+
entries, err := os.ReadDir(dir)
|
|
80
|
+
if err != nil {
|
|
81
|
+
t.Fatal(err)
|
|
82
|
+
}
|
|
83
|
+
for _, e := range entries {
|
|
84
|
+
if strings.HasPrefix(e.Name(), ".tmp-") && strings.HasSuffix(e.Name(), ".write") {
|
|
85
|
+
t.Errorf("temp file %q left behind after successful write", e.Name())
|
|
86
|
+
}
|
|
87
|
+
if strings.HasSuffix(e.Name(), ".savepoint-bak") {
|
|
88
|
+
t.Errorf("backup file %q left behind after successful write", e.Name())
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
package styles
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"testing"
|
|
5
|
+
|
|
6
|
+
"github.com/charmbracelet/lipgloss"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
func TestPaletteConstants_present(t *testing.T) {
|
|
10
|
+
// Truecolor tier
|
|
11
|
+
if Background == "" {
|
|
12
|
+
t.Error("Background constant is empty")
|
|
13
|
+
}
|
|
14
|
+
if Surface == "" {
|
|
15
|
+
t.Error("Surface constant is empty")
|
|
16
|
+
}
|
|
17
|
+
if Surface2 == "" {
|
|
18
|
+
t.Error("Surface2 constant is empty")
|
|
19
|
+
}
|
|
20
|
+
if Border == "" {
|
|
21
|
+
t.Error("Border constant is empty")
|
|
22
|
+
}
|
|
23
|
+
if BorderSubtle == "" {
|
|
24
|
+
t.Error("BorderSubtle constant is empty")
|
|
25
|
+
}
|
|
26
|
+
if PrimaryText == "" {
|
|
27
|
+
t.Error("PrimaryText constant is empty")
|
|
28
|
+
}
|
|
29
|
+
if AtariOrange == "" {
|
|
30
|
+
t.Error("AtariOrange constant is empty")
|
|
31
|
+
}
|
|
32
|
+
if NPPGreen == "" {
|
|
33
|
+
t.Error("NPPGreen constant is empty")
|
|
34
|
+
}
|
|
35
|
+
if VibePurple == "" {
|
|
36
|
+
t.Error("VibePurple constant is empty")
|
|
37
|
+
}
|
|
38
|
+
if Dim == "" {
|
|
39
|
+
t.Error("Dim constant is empty")
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func TestPaletteConstants_256tier(t *testing.T) {
|
|
44
|
+
if Background256 == "" {
|
|
45
|
+
t.Error("Background256 constant is empty")
|
|
46
|
+
}
|
|
47
|
+
if Surface256 == "" {
|
|
48
|
+
t.Error("Surface256 constant is empty")
|
|
49
|
+
}
|
|
50
|
+
if Surface2256 == "" {
|
|
51
|
+
t.Error("Surface2256 constant is empty")
|
|
52
|
+
}
|
|
53
|
+
if Border256 == "" {
|
|
54
|
+
t.Error("Border256 constant is empty")
|
|
55
|
+
}
|
|
56
|
+
if BorderSubtle256 == "" {
|
|
57
|
+
t.Error("BorderSubtle256 constant is empty")
|
|
58
|
+
}
|
|
59
|
+
if PrimaryText256 == "" {
|
|
60
|
+
t.Error("PrimaryText256 constant is empty")
|
|
61
|
+
}
|
|
62
|
+
if AtariOrange256 == "" {
|
|
63
|
+
t.Error("AtariOrange256 constant is empty")
|
|
64
|
+
}
|
|
65
|
+
if NPPGreen256 == "" {
|
|
66
|
+
t.Error("NPPGreen256 constant is empty")
|
|
67
|
+
}
|
|
68
|
+
if VibePurple256 == "" {
|
|
69
|
+
t.Error("VibePurple256 constant is empty")
|
|
70
|
+
}
|
|
71
|
+
if Dim256 == "" {
|
|
72
|
+
t.Error("Dim256 constant is empty")
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func TestPaletteConstants_16tier(t *testing.T) {
|
|
77
|
+
if Background16 == "" {
|
|
78
|
+
t.Error("Background16 constant is empty")
|
|
79
|
+
}
|
|
80
|
+
if Surface16 == "" {
|
|
81
|
+
t.Error("Surface16 constant is empty")
|
|
82
|
+
}
|
|
83
|
+
if Surface216 == "" {
|
|
84
|
+
t.Error("Surface216 constant is empty")
|
|
85
|
+
}
|
|
86
|
+
if Border16 == "" {
|
|
87
|
+
t.Error("Border16 constant is empty")
|
|
88
|
+
}
|
|
89
|
+
if BorderSubtle16 == "" {
|
|
90
|
+
t.Error("BorderSubtle16 constant is empty")
|
|
91
|
+
}
|
|
92
|
+
if PrimaryText16 == "" {
|
|
93
|
+
t.Error("PrimaryText16 constant is empty")
|
|
94
|
+
}
|
|
95
|
+
if AtariOrange16 == "" {
|
|
96
|
+
t.Error("AtariOrange16 constant is empty")
|
|
97
|
+
}
|
|
98
|
+
if NPPGreen16 == "" {
|
|
99
|
+
t.Error("NPPGreen16 constant is empty")
|
|
100
|
+
}
|
|
101
|
+
if VibePurple16 == "" {
|
|
102
|
+
t.Error("VibePurple16 constant is empty")
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
func TestColor(t *testing.T) {
|
|
107
|
+
c := color("#FF0000", "196", "9")
|
|
108
|
+
expected := lipgloss.CompleteColor{TrueColor: "#FF0000", ANSI256: "196", ANSI: "9"}
|
|
109
|
+
if c != expected {
|
|
110
|
+
t.Errorf("color() = %+v, want %+v", c, expected)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
func TestColor_usesPaletteConstants(t *testing.T) {
|
|
115
|
+
c := color(AtariOrange, AtariOrange256, AtariOrange16)
|
|
116
|
+
if c.TrueColor != AtariOrange {
|
|
117
|
+
t.Errorf("color TrueColor = %q, want %q", c.TrueColor, AtariOrange)
|
|
118
|
+
}
|
|
119
|
+
if c.ANSI256 != AtariOrange256 {
|
|
120
|
+
t.Errorf("color ANSI256 = %q, want %q", c.ANSI256, AtariOrange256)
|
|
121
|
+
}
|
|
122
|
+
if c.ANSI != AtariOrange16 {
|
|
123
|
+
t.Errorf("color ANSI = %q, want %q", c.ANSI, AtariOrange16)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func TestColor_returnsCompleteColor(t *testing.T) {
|
|
128
|
+
var cc lipgloss.CompleteColor
|
|
129
|
+
cc = color(Background, Background256, Background16)
|
|
130
|
+
if cc.TrueColor != Background {
|
|
131
|
+
t.Errorf("expected TrueColor %q, got %q", Background, cc.TrueColor)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
package testutil
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// WriteRouter writes a router.md at root with the given fields.
|
|
11
|
+
// If nextAction is empty, it defaults to "".
|
|
12
|
+
func WriteRouter(t testing.TB, root, state, release, epic, task, nextAction string) {
|
|
13
|
+
t.Helper()
|
|
14
|
+
if nextAction == "" {
|
|
15
|
+
nextAction = `""`
|
|
16
|
+
} else {
|
|
17
|
+
nextAction = `"` + nextAction + `"`
|
|
18
|
+
}
|
|
19
|
+
if task == "" {
|
|
20
|
+
task = `""`
|
|
21
|
+
} else {
|
|
22
|
+
task = `"` + task + `"`
|
|
23
|
+
}
|
|
24
|
+
content := "# Agent State Machine\n\n## Current state\n\n```yaml\nstate: " + state + "\nrelease: " + release + "\nepic: " + epic + "\ntask: " + task + "\nnext_action: " + nextAction + "\n```\n"
|
|
25
|
+
WriteFile(t, filepath.Join(root, "router.md"), content)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// WriteReleasePRD writes a minimal release PRD file.
|
|
29
|
+
func WriteReleasePRD(t testing.TB, releasePath string) {
|
|
30
|
+
t.Helper()
|
|
31
|
+
releaseID := filepath.Base(releasePath)
|
|
32
|
+
WriteFile(t, filepath.Join(releasePath, releaseID+"-PRD.md"), "---\ntype: project-prd\nstatus: active\n---\n\n# Release\n")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// WriteEpicDetail writes a minimal epic detail file.
|
|
36
|
+
func WriteEpicDetail(t testing.TB, epicPath, prefix string) {
|
|
37
|
+
t.Helper()
|
|
38
|
+
WriteFile(t, filepath.Join(epicPath, prefix+"-Detail.md"), "---\ntype: epic-design\nstatus: planned\n---\n\n# Epic\n")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// TaskFixture describes a task file to create.
|
|
42
|
+
type TaskFixture struct {
|
|
43
|
+
Slug string // e.g. "T001-task" — becomes filename
|
|
44
|
+
Release string // optional; omitted if empty
|
|
45
|
+
Status string
|
|
46
|
+
Phase string // optional; omitted if empty
|
|
47
|
+
Objective string
|
|
48
|
+
DependsOn []string // optional; defaults to empty list
|
|
49
|
+
Body string // optional; defaults to minimal body
|
|
50
|
+
Extra map[string]string // optional; extra frontmatter fields
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// WriteTask writes a task file in the savepoint directory structure.
|
|
54
|
+
func WriteTask(t testing.TB, root, release, epic string, task TaskFixture) {
|
|
55
|
+
t.Helper()
|
|
56
|
+
path := filepath.Join(root, "releases", release, "epics", epic, "tasks", task.Slug+".md")
|
|
57
|
+
id := epic + "/" + task.Slug
|
|
58
|
+
|
|
59
|
+
var b strings.Builder
|
|
60
|
+
b.WriteString("---\n")
|
|
61
|
+
b.WriteString("id: " + id + "\n")
|
|
62
|
+
if task.Release != "" {
|
|
63
|
+
b.WriteString("release: " + task.Release + "\n")
|
|
64
|
+
}
|
|
65
|
+
b.WriteString("status: " + task.Status + "\n")
|
|
66
|
+
if task.Phase != "" {
|
|
67
|
+
b.WriteString("phase: " + task.Phase + "\n")
|
|
68
|
+
}
|
|
69
|
+
b.WriteString("objective: \"" + task.Objective + "\"\n")
|
|
70
|
+
|
|
71
|
+
deps := "[]"
|
|
72
|
+
if len(task.DependsOn) > 0 {
|
|
73
|
+
quoted := make([]string, len(task.DependsOn))
|
|
74
|
+
for i, d := range task.DependsOn {
|
|
75
|
+
quoted[i] = fmt.Sprintf("%q", d)
|
|
76
|
+
}
|
|
77
|
+
deps = "[" + strings.Join(quoted, ", ") + "]"
|
|
78
|
+
}
|
|
79
|
+
b.WriteString("depends_on: " + deps + "\n")
|
|
80
|
+
for k, v := range task.Extra {
|
|
81
|
+
b.WriteString(k + ": " + v + "\n")
|
|
82
|
+
}
|
|
83
|
+
b.WriteString("---\n\n")
|
|
84
|
+
|
|
85
|
+
if task.Body != "" {
|
|
86
|
+
b.WriteString(task.Body)
|
|
87
|
+
} else {
|
|
88
|
+
b.WriteString("# " + task.Slug + "\n\n## Acceptance Criteria\n\n- it works\n")
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
WriteFile(t, path, b.String())
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// SetupMinimalProject creates a minimal valid savepoint project structure.
|
|
95
|
+
// It creates config.yml, router.md, release PRD, epic detail, and the directory tree.
|
|
96
|
+
func SetupMinimalProject(t testing.TB, root, release, epic string) {
|
|
97
|
+
t.Helper()
|
|
98
|
+
releasePath := filepath.Join(root, "releases", release)
|
|
99
|
+
//nolint:gocritic // assignment to same epicPath variable is fine
|
|
100
|
+
epicPath := filepath.Join(releasePath, "epics", epic)
|
|
101
|
+
tasksPath := filepath.Join(epicPath, "tasks")
|
|
102
|
+
MkdirAll(t, tasksPath)
|
|
103
|
+
|
|
104
|
+
prefix := epic
|
|
105
|
+
if idx := strings.IndexByte(epic, '-'); idx != -1 {
|
|
106
|
+
prefix = epic[:idx]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
WriteFile(t, filepath.Join(root, "config.yml"), "quality_gates:\n lint: null\n typecheck: null\n test: null\ntheme:\n bg: \"#000\"\n")
|
|
110
|
+
WriteRouter(t, root, "task-building", release, epic, "", "")
|
|
111
|
+
WriteReleasePRD(t, releasePath)
|
|
112
|
+
WriteEpicDetail(t, epicPath, prefix)
|
|
113
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
package testutil
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// WriteFile writes content to path, creating parent directories if needed.
|
|
10
|
+
func WriteFile(t testing.TB, path, content string) {
|
|
11
|
+
t.Helper()
|
|
12
|
+
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
13
|
+
t.Fatal(err)
|
|
14
|
+
}
|
|
15
|
+
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
|
16
|
+
t.Fatal(err)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// MkdirAll creates directories with mode 0755, fatal on error.
|
|
21
|
+
func MkdirAll(t testing.TB, path string) {
|
|
22
|
+
t.Helper()
|
|
23
|
+
if err := os.MkdirAll(path, 0755); err != nil {
|
|
24
|
+
t.Fatal(err)
|
|
25
|
+
}
|
|
26
|
+
}
|
package/main.go
CHANGED
|
@@ -1,20 +1,136 @@
|
|
|
1
1
|
package main
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"context"
|
|
5
|
+
"embed"
|
|
4
6
|
"fmt"
|
|
7
|
+
"io/fs"
|
|
5
8
|
"os"
|
|
6
9
|
|
|
10
|
+
"github.com/opencode/savepoint/cmd"
|
|
7
11
|
"github.com/opencode/savepoint/internal/board"
|
|
12
|
+
"github.com/opencode/savepoint/internal/data"
|
|
13
|
+
"github.com/opencode/savepoint/internal/doctor"
|
|
14
|
+
savepointinit "github.com/opencode/savepoint/internal/init"
|
|
8
15
|
)
|
|
9
16
|
|
|
17
|
+
//go:embed templates/project
|
|
18
|
+
//go:embed templates/project/.savepoint
|
|
19
|
+
//go:embed templates/prompts
|
|
20
|
+
var projectTemplates embed.FS
|
|
21
|
+
|
|
10
22
|
var version = "dev"
|
|
11
23
|
|
|
12
24
|
func main() {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
25
|
+
args, debug := stripDebugFlag(os.Args[1:])
|
|
26
|
+
if debug || os.Getenv("SAVEPOINT_DEBUG") != "" {
|
|
27
|
+
board.SetDebug(true)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if len(args) > 0 {
|
|
31
|
+
switch args[0] {
|
|
32
|
+
case "--version":
|
|
33
|
+
fmt.Println(version)
|
|
34
|
+
os.Exit(0)
|
|
35
|
+
case "init":
|
|
36
|
+
if err := cmd.RunInit(context.Background(), args[1:], os.Stdout, initRunner); err != nil {
|
|
37
|
+
fmt.Fprintln(os.Stderr, err)
|
|
38
|
+
os.Exit(1)
|
|
39
|
+
}
|
|
40
|
+
os.Exit(0)
|
|
41
|
+
case "board":
|
|
42
|
+
if err := cmd.RunBoard(context.Background(), args[1:], os.Stdout, func(opts cmd.BoardOptions) error {
|
|
43
|
+
return board.RunWithFilters(opts.Release, opts.Epic)
|
|
44
|
+
}); err != nil {
|
|
45
|
+
fmt.Fprintln(os.Stderr, err)
|
|
46
|
+
os.Exit(1)
|
|
47
|
+
}
|
|
48
|
+
os.Exit(0)
|
|
49
|
+
case "doctor":
|
|
50
|
+
code, err := cmd.RunDoctor(context.Background(), args[1:], os.Stdout, func(opts cmd.DoctorOptions) (int, error) {
|
|
51
|
+
return runDoctorChecks(opts)
|
|
52
|
+
})
|
|
53
|
+
if err != nil {
|
|
54
|
+
fmt.Fprintln(os.Stderr, err)
|
|
55
|
+
}
|
|
56
|
+
os.Exit(code)
|
|
57
|
+
}
|
|
16
58
|
}
|
|
17
59
|
if err := board.Run(); err != nil {
|
|
18
60
|
panic(err)
|
|
19
61
|
}
|
|
20
|
-
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// stripDebugFlag removes --debug from args and reports whether it was present.
|
|
65
|
+
func stripDebugFlag(args []string) ([]string, bool) {
|
|
66
|
+
out := make([]string, 0, len(args))
|
|
67
|
+
found := false
|
|
68
|
+
for _, a := range args {
|
|
69
|
+
if a == "--debug" {
|
|
70
|
+
found = true
|
|
71
|
+
} else {
|
|
72
|
+
out = append(out, a)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return out, found
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
func runDoctorChecks(opts cmd.DoctorOptions) (int, error) {
|
|
79
|
+
discover := data.NewDiscover()
|
|
80
|
+
root, err := discover.FindSavepointRoot(".")
|
|
81
|
+
if err != nil {
|
|
82
|
+
return 2, fmt.Errorf("savepoint root not found: %w", err)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
report := doctor.RunAllChecks(root, opts.Epic)
|
|
86
|
+
fmt.Fprint(os.Stdout, report.Format())
|
|
87
|
+
|
|
88
|
+
if report.HasProblems() {
|
|
89
|
+
return 1, nil
|
|
90
|
+
}
|
|
91
|
+
return 0, nil
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func initRunner(ctx context.Context, opts cmd.InitOptions) error {
|
|
95
|
+
if err := savepointinit.ValidateTarget(opts.Dir, opts.Force); err != nil {
|
|
96
|
+
return err
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
sub, err := fs.Sub(projectTemplates, "templates/project")
|
|
100
|
+
if err != nil {
|
|
101
|
+
return fmt.Errorf("cannot load templates: %w", err)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
projectName := savepointinit.ProjectNameFromDir(opts.Dir)
|
|
105
|
+
if err := savepointinit.Scaffold(sub, opts.Dir, projectName, opts.Force); err != nil {
|
|
106
|
+
return err
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
promptSub, err := fs.Sub(projectTemplates, "templates/prompts")
|
|
110
|
+
if err != nil {
|
|
111
|
+
return fmt.Errorf("cannot load prompt templates: %w", err)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
prompt, err := savepointinit.RenderMagicPrompt(promptSub, projectName)
|
|
115
|
+
if err != nil {
|
|
116
|
+
return fmt.Errorf("render magic prompt: %w", err)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fmt.Println(prompt)
|
|
120
|
+
|
|
121
|
+
result := savepointinit.CopyToClipboard(prompt)
|
|
122
|
+
switch result.Status {
|
|
123
|
+
case savepointinit.ClipboardCopied:
|
|
124
|
+
fmt.Fprintf(os.Stderr, "prompt copied to clipboard via %s\n", result.Tool)
|
|
125
|
+
case savepointinit.ClipboardFailed:
|
|
126
|
+
fmt.Fprintf(os.Stderr, "warning: clipboard copy failed: %s\n", result.Message)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if opts.Install {
|
|
130
|
+
if err := savepointinit.InstallDependencies(opts.Dir); err != nil {
|
|
131
|
+
return err
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return nil
|
|
136
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "savepoint",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "It’s a simple, file-based state machine and cinematic Terminal UI (TUI) designed to force you—and your agent (Claude, Cursor, Aider, Gemini)—to slow down, write down what you're actually building, and check your work before moving on.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"board",
|
|
@@ -21,6 +21,6 @@
|
|
|
21
21
|
"savepoint": "./savepoint"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
|
-
"test": "
|
|
24
|
+
"test": "echo \"Run 'make test' for Go tests\""
|
|
25
25
|
}
|
|
26
26
|
}
|