savepoint 1.0.2 → 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.
- package/.claude/settings.local.json +12 -1
- package/.golangci.yml +11 -0
- package/.savepoint/Design.md +37 -36
- 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-Detail.md +43 -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 +28 -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 +28 -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 +9 -16
- package/AGENTS.md +38 -23
- 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 +40 -36
- package/internal/board/board_test.go +27 -82
- package/internal/board/card.go +43 -23
- package/internal/board/card_test.go +41 -5
- package/internal/board/column.go +44 -13
- package/internal/board/column_test.go +5 -2
- package/internal/board/detail.go +0 -47
- package/internal/board/epic_panel.go +118 -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 +325 -215
- package/internal/board/update_test.go +299 -18
- package/internal/board/util.go +76 -0
- package/internal/board/view.go +31 -28
- package/internal/board/view_test.go +12 -2
- package/internal/board/watch.go +35 -5
- package/internal/buildtool/main.go +2 -10
- package/internal/buildtool/main_test.go +46 -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/lifecycle.go +13 -6
- package/internal/data/lifecycle_test.go +14 -11
- package/internal/data/parser.go +21 -6
- package/internal/data/parser_test.go +31 -7
- package/internal/data/task.go +0 -9
- package/internal/data/write.go +85 -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 +101 -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 +406 -0
- package/project-audit/consolidated-audit-report.md +456 -0
- package/savepoint +0 -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.exe +0 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
package doctor
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"strings"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// QualityGateReport wraps quality gate results.
|
|
9
|
+
type QualityGateReport struct {
|
|
10
|
+
Results []GateResult
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// DiagnosticReport is the complete output of all doctor checks.
|
|
14
|
+
type DiagnosticReport struct {
|
|
15
|
+
ConfigCheck error
|
|
16
|
+
RouterCheck error
|
|
17
|
+
Structure []Problem
|
|
18
|
+
Dependencies []Problem
|
|
19
|
+
AuditState []Problem
|
|
20
|
+
Orphans []Problem
|
|
21
|
+
Gates QualityGateReport
|
|
22
|
+
EpicFilter string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// RunAllChecks runs every doctor check and returns a full report.
|
|
26
|
+
func RunAllChecks(root string, epicFilter string) *DiagnosticReport {
|
|
27
|
+
report := &DiagnosticReport{
|
|
28
|
+
EpicFilter: epicFilter,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
report.ConfigCheck = CheckConfig(root)
|
|
32
|
+
report.RouterCheck = CheckRouter(root, epicFilter)
|
|
33
|
+
report.Structure = CheckStructure(root, epicFilter)
|
|
34
|
+
report.Dependencies = CheckDependencies(root, epicFilter)
|
|
35
|
+
report.AuditState = CheckAuditState(root)
|
|
36
|
+
report.Orphans = CheckOrphans(root)
|
|
37
|
+
report.Gates.Results = RunQualityGates(root)
|
|
38
|
+
|
|
39
|
+
return report
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// HasProblems returns true if any check found issues.
|
|
43
|
+
func (r *DiagnosticReport) HasProblems() bool {
|
|
44
|
+
if r.ConfigCheck != nil {
|
|
45
|
+
return true
|
|
46
|
+
}
|
|
47
|
+
if r.RouterCheck != nil {
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
if len(r.Structure) > 0 {
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
if len(r.Dependencies) > 0 {
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
56
|
+
if len(r.AuditState) > 0 {
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
if len(r.Orphans) > 0 {
|
|
60
|
+
return true
|
|
61
|
+
}
|
|
62
|
+
for _, g := range r.Gates.Results {
|
|
63
|
+
if !g.Passed {
|
|
64
|
+
return true
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Format produces the full human-readable diagnostic report.
|
|
71
|
+
func (r *DiagnosticReport) Format() string {
|
|
72
|
+
var b strings.Builder
|
|
73
|
+
|
|
74
|
+
b.WriteString("savepoint doctor report\n")
|
|
75
|
+
if r.EpicFilter != "" {
|
|
76
|
+
fmt.Fprintf(&b, " filtering to epic: %s\n", r.EpicFilter)
|
|
77
|
+
}
|
|
78
|
+
b.WriteString("────────────────────────────────\n\n")
|
|
79
|
+
|
|
80
|
+
sectionHeader(&b, "Config Check")
|
|
81
|
+
printSingleCheck(&b, "config", r.ConfigCheck)
|
|
82
|
+
|
|
83
|
+
sectionHeader(&b, "Router Check")
|
|
84
|
+
printSingleCheck(&b, "router", r.RouterCheck)
|
|
85
|
+
|
|
86
|
+
sectionHeader(&b, "Structure Check")
|
|
87
|
+
printProblems(&b, "structure", r.Structure)
|
|
88
|
+
|
|
89
|
+
sectionHeader(&b, "Dependency Check")
|
|
90
|
+
printProblems(&b, "dependency", r.Dependencies)
|
|
91
|
+
|
|
92
|
+
sectionHeader(&b, "Audit State Check")
|
|
93
|
+
printProblems(&b, "audit", r.AuditState)
|
|
94
|
+
|
|
95
|
+
sectionHeader(&b, "Orphan Check")
|
|
96
|
+
printProblems(&b, "orphan", r.Orphans)
|
|
97
|
+
|
|
98
|
+
sectionHeader(&b, "Quality Gates")
|
|
99
|
+
for _, g := range r.Gates.Results {
|
|
100
|
+
status := "PASS"
|
|
101
|
+
if !g.Passed {
|
|
102
|
+
status = "FAIL"
|
|
103
|
+
}
|
|
104
|
+
fmt.Fprintf(&b, " [%s] %s", status, g.Name)
|
|
105
|
+
if g.Command != "" {
|
|
106
|
+
fmt.Fprintf(&b, " (%s)", g.Command)
|
|
107
|
+
}
|
|
108
|
+
b.WriteString("\n")
|
|
109
|
+
if !g.Passed {
|
|
110
|
+
hint := GateSuggestion(g.Name)
|
|
111
|
+
fmt.Fprintf(&b, " repair: %s\n", hint)
|
|
112
|
+
if g.Output != "" {
|
|
113
|
+
for _, line := range strings.Split(g.Output, "\n") {
|
|
114
|
+
b.WriteString(" ")
|
|
115
|
+
b.WriteString(line)
|
|
116
|
+
b.WriteString("\n")
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
b.WriteString("\n")
|
|
123
|
+
if r.HasProblems() {
|
|
124
|
+
b.WriteString("result: PROBLEMS FOUND (exit code 1)\n")
|
|
125
|
+
} else {
|
|
126
|
+
b.WriteString("result: ALL CLEAN (exit code 0)\n")
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return b.String()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
func sectionHeader(b *strings.Builder, title string) {
|
|
133
|
+
fmt.Fprintf(b, "◆ %s\n", title)
|
|
134
|
+
b.WriteString(strings.Repeat("─", len(title)+2))
|
|
135
|
+
b.WriteString("\n")
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
func printSingleCheck(b *strings.Builder, name string, err error) {
|
|
139
|
+
if err == nil {
|
|
140
|
+
fmt.Fprintf(b, " ✓ %s\n\n", name)
|
|
141
|
+
} else {
|
|
142
|
+
fmt.Fprintf(b, " ✗ %s: %s\n", name, err.Error())
|
|
143
|
+
fmt.Fprintf(b, " repair: %s\n\n", SuggestRepair(err))
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
func printProblems(b *strings.Builder, category string, problems []Problem) {
|
|
148
|
+
if len(problems) == 0 {
|
|
149
|
+
fmt.Fprintf(b, " ✓ no problems\n\n")
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
for _, p := range problems {
|
|
153
|
+
fmt.Fprintf(b, " ✗ %s: %s\n", category, p.Error())
|
|
154
|
+
fmt.Fprintf(b, " repair: %s\n", SuggestRepair(p))
|
|
155
|
+
}
|
|
156
|
+
b.WriteString("\n")
|
|
157
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
package doctor
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/opencode/savepoint/internal/testutil"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func TestDiagnosticReport_HasProblems(t *testing.T) {
|
|
11
|
+
root := t.TempDir()
|
|
12
|
+
report := RunAllChecks(root, "")
|
|
13
|
+
if !report.HasProblems() {
|
|
14
|
+
t.Fatal("RunAllChecks() on empty dir should have problems")
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func TestDiagnosticReport_CleanProject(t *testing.T) {
|
|
19
|
+
root := t.TempDir()
|
|
20
|
+
writeReportProject(t, root)
|
|
21
|
+
report := RunAllChecks(root, "")
|
|
22
|
+
if report.HasProblems() {
|
|
23
|
+
t.Fatalf("RunAllChecks() on valid project should have no problems, got: config=%v router=%v structure=%v deps=%v audit=%v orphans=%v gates=%v",
|
|
24
|
+
report.ConfigCheck, report.RouterCheck, report.Structure, report.Dependencies, report.AuditState, report.Orphans, report.Gates.Results)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func TestDiagnosticReport_FormatContainsSections(t *testing.T) {
|
|
29
|
+
root := t.TempDir()
|
|
30
|
+
report := RunAllChecks(root, "")
|
|
31
|
+
output := report.Format()
|
|
32
|
+
|
|
33
|
+
sections := []string{
|
|
34
|
+
"Config Check",
|
|
35
|
+
"Router Check",
|
|
36
|
+
"Structure Check",
|
|
37
|
+
"Dependency Check",
|
|
38
|
+
"Audit State Check",
|
|
39
|
+
"Orphan Check",
|
|
40
|
+
"Quality Gates",
|
|
41
|
+
"PROBLEMS FOUND",
|
|
42
|
+
}
|
|
43
|
+
for _, s := range sections {
|
|
44
|
+
if !strings.Contains(output, s) {
|
|
45
|
+
t.Errorf("report.Format() missing section %q", s)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func TestDiagnosticReport_FormatWithEpicFilter(t *testing.T) {
|
|
51
|
+
root := t.TempDir()
|
|
52
|
+
report := RunAllChecks(root, "E03")
|
|
53
|
+
output := report.Format()
|
|
54
|
+
if !strings.Contains(output, "filtering to epic: E03") {
|
|
55
|
+
t.Errorf("report.Format() missing epic filter: %s", output)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func TestDiagnosticReport_FormatAllClean(t *testing.T) {
|
|
60
|
+
root := t.TempDir()
|
|
61
|
+
writeReportProject(t, root)
|
|
62
|
+
report := RunAllChecks(root, "")
|
|
63
|
+
output := report.Format()
|
|
64
|
+
if !strings.Contains(output, "ALL CLEAN") {
|
|
65
|
+
t.Errorf("report.Format() on clean project should say ALL CLEAN, got: %s", output)
|
|
66
|
+
}
|
|
67
|
+
if strings.Contains(output, "PROBLEMS FOUND") {
|
|
68
|
+
t.Errorf("report.Format() on clean project should not say PROBLEMS FOUND, got: %s", output)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func TestDiagnosticReport_FormatShowsRepairs(t *testing.T) {
|
|
73
|
+
root := t.TempDir()
|
|
74
|
+
report := RunAllChecks(root, "")
|
|
75
|
+
output := report.Format()
|
|
76
|
+
if !strings.Contains(output, "repair:") {
|
|
77
|
+
t.Errorf("report.Format() should include repair suggestions, got: %s", output)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func writeReportProject(t *testing.T, root string) {
|
|
82
|
+
t.Helper()
|
|
83
|
+
testutil.SetupMinimalProject(t, root, "v1", "E01-foo")
|
|
84
|
+
testutil.WriteTask(t, root, "v1", "E01-foo", testutil.TaskFixture{
|
|
85
|
+
Slug: "T001-task",
|
|
86
|
+
Status: "planned",
|
|
87
|
+
Objective: "Task",
|
|
88
|
+
})
|
|
89
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
package init
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os/exec"
|
|
7
|
+
"runtime"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
type ClipboardStatus int
|
|
11
|
+
|
|
12
|
+
const (
|
|
13
|
+
ClipboardCopied ClipboardStatus = iota
|
|
14
|
+
ClipboardSkipped
|
|
15
|
+
ClipboardFailed
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
func (s ClipboardStatus) String() string {
|
|
19
|
+
switch s {
|
|
20
|
+
case ClipboardCopied:
|
|
21
|
+
return "copied"
|
|
22
|
+
case ClipboardSkipped:
|
|
23
|
+
return "skipped"
|
|
24
|
+
case ClipboardFailed:
|
|
25
|
+
return "failed"
|
|
26
|
+
default:
|
|
27
|
+
return "unknown"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type ClipboardResult struct {
|
|
32
|
+
Status ClipboardStatus
|
|
33
|
+
Tool string
|
|
34
|
+
Message string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func CopyToClipboard(text string) ClipboardResult {
|
|
38
|
+
switch runtime.GOOS {
|
|
39
|
+
case "windows":
|
|
40
|
+
return copyWindows(text)
|
|
41
|
+
case "darwin":
|
|
42
|
+
return copyDarwin(text)
|
|
43
|
+
case "linux":
|
|
44
|
+
return copyLinux(text)
|
|
45
|
+
default:
|
|
46
|
+
return ClipboardResult{
|
|
47
|
+
Status: ClipboardSkipped,
|
|
48
|
+
Tool: "",
|
|
49
|
+
Message: fmt.Sprintf("unsupported platform: %s", runtime.GOOS),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func copyWindows(text string) ClipboardResult {
|
|
55
|
+
if _, err := exec.LookPath("clip"); err != nil {
|
|
56
|
+
return ClipboardResult{
|
|
57
|
+
Status: ClipboardSkipped,
|
|
58
|
+
Tool: "clip.exe",
|
|
59
|
+
Message: "clip.exe not found in PATH",
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
cmd := exec.Command("clip")
|
|
64
|
+
cmd.Stdin = bytes.NewReader([]byte(text))
|
|
65
|
+
if err := cmd.Run(); err != nil {
|
|
66
|
+
return ClipboardResult{
|
|
67
|
+
Status: ClipboardFailed,
|
|
68
|
+
Tool: "clip.exe",
|
|
69
|
+
Message: fmt.Sprintf("clip.exe failed: %v", err),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return ClipboardResult{
|
|
74
|
+
Status: ClipboardCopied,
|
|
75
|
+
Tool: "clip.exe",
|
|
76
|
+
Message: "",
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func copyDarwin(text string) ClipboardResult {
|
|
81
|
+
if _, err := exec.LookPath("pbcopy"); err != nil {
|
|
82
|
+
return ClipboardResult{
|
|
83
|
+
Status: ClipboardSkipped,
|
|
84
|
+
Tool: "pbcopy",
|
|
85
|
+
Message: "pbcopy not found in PATH",
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
cmd := exec.Command("pbcopy")
|
|
90
|
+
cmd.Stdin = bytes.NewReader([]byte(text))
|
|
91
|
+
if err := cmd.Run(); err != nil {
|
|
92
|
+
return ClipboardResult{
|
|
93
|
+
Status: ClipboardFailed,
|
|
94
|
+
Tool: "pbcopy",
|
|
95
|
+
Message: fmt.Sprintf("pbcopy failed: %v", err),
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return ClipboardResult{
|
|
100
|
+
Status: ClipboardCopied,
|
|
101
|
+
Tool: "pbcopy",
|
|
102
|
+
Message: "",
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
func copyLinux(text string) ClipboardResult {
|
|
107
|
+
if path, err := exec.LookPath("xclip"); err == nil {
|
|
108
|
+
cmd := exec.Command(path, "-selection", "clipboard")
|
|
109
|
+
cmd.Stdin = bytes.NewReader([]byte(text))
|
|
110
|
+
if err := cmd.Run(); err != nil {
|
|
111
|
+
return ClipboardResult{
|
|
112
|
+
Status: ClipboardFailed,
|
|
113
|
+
Tool: "xclip",
|
|
114
|
+
Message: fmt.Sprintf("xclip failed: %v", err),
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return ClipboardResult{
|
|
118
|
+
Status: ClipboardCopied,
|
|
119
|
+
Tool: "xclip",
|
|
120
|
+
Message: "",
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if path, err := exec.LookPath("xsel"); err == nil {
|
|
125
|
+
cmd := exec.Command(path, "--input", "--clipboard")
|
|
126
|
+
cmd.Stdin = bytes.NewReader([]byte(text))
|
|
127
|
+
if err := cmd.Run(); err != nil {
|
|
128
|
+
return ClipboardResult{
|
|
129
|
+
Status: ClipboardFailed,
|
|
130
|
+
Tool: "xsel",
|
|
131
|
+
Message: fmt.Sprintf("xsel failed: %v", err),
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return ClipboardResult{
|
|
135
|
+
Status: ClipboardCopied,
|
|
136
|
+
Tool: "xsel",
|
|
137
|
+
Message: "",
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return ClipboardResult{
|
|
142
|
+
Status: ClipboardSkipped,
|
|
143
|
+
Tool: "",
|
|
144
|
+
Message: "no clipboard tool found (tried xclip, xsel)",
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
package init
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"runtime"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
func TestCopyToClipboard_returnsWithoutPanic(t *testing.T) {
|
|
9
|
+
result := CopyToClipboard("test content")
|
|
10
|
+
if result.Status != ClipboardCopied &&
|
|
11
|
+
result.Status != ClipboardSkipped &&
|
|
12
|
+
result.Status != ClipboardFailed {
|
|
13
|
+
t.Fatalf("CopyToClipboard() returned unexpected status %v", result.Status)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func TestCopyToClipboard_resultHasNonEmptyStatusString(t *testing.T) {
|
|
18
|
+
result := CopyToClipboard("test")
|
|
19
|
+
if result.Status.String() == "" {
|
|
20
|
+
t.Fatal("Status.String() returned empty")
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func TestCopyToClipboard_skippedOnUnsupportedPlatform(t *testing.T) {
|
|
25
|
+
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" || runtime.GOOS == "linux" {
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
result := CopyToClipboard("test")
|
|
29
|
+
if result.Status != ClipboardSkipped {
|
|
30
|
+
t.Fatalf("on unsupported platform, expected ClipboardSkipped, got %v", result.Status)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func TestCopyToClipboard_emptyString(t *testing.T) {
|
|
35
|
+
result := CopyToClipboard("")
|
|
36
|
+
if result.Status != ClipboardCopied &&
|
|
37
|
+
result.Status != ClipboardSkipped &&
|
|
38
|
+
result.Status != ClipboardFailed {
|
|
39
|
+
t.Fatalf("CopyToClipboard('') returned unexpected status %v", result.Status)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func TestCopyToClipboard_toolName(t *testing.T) {
|
|
44
|
+
result := CopyToClipboard("test")
|
|
45
|
+
if result.Status == ClipboardCopied && result.Tool == "" {
|
|
46
|
+
t.Fatal("ClipboardCopied but Tool is empty")
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func TestClipboardStatus_string(t *testing.T) {
|
|
51
|
+
tests := []struct {
|
|
52
|
+
status ClipboardStatus
|
|
53
|
+
want string
|
|
54
|
+
}{
|
|
55
|
+
{ClipboardCopied, "copied"},
|
|
56
|
+
{ClipboardSkipped, "skipped"},
|
|
57
|
+
{ClipboardFailed, "failed"},
|
|
58
|
+
}
|
|
59
|
+
for _, tt := range tests {
|
|
60
|
+
if got := tt.status.String(); got != tt.want {
|
|
61
|
+
t.Errorf("ClipboardStatus(%d).String() = %q, want %q", tt.status, got, tt.want)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func TestCopyToClipboard_multipleCalls(t *testing.T) {
|
|
67
|
+
for i := 0; i < 3; i++ {
|
|
68
|
+
result := CopyToClipboard("test content")
|
|
69
|
+
if result.Status != ClipboardCopied &&
|
|
70
|
+
result.Status != ClipboardSkipped {
|
|
71
|
+
t.Fatalf("call %d: unexpected status %v", i, result.Status)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package init
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os/exec"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
func InstallDependencies(dir string) error {
|
|
9
|
+
cmd := exec.Command("npm", "install")
|
|
10
|
+
cmd.Dir = dir
|
|
11
|
+
out, err := cmd.CombinedOutput()
|
|
12
|
+
if err != nil {
|
|
13
|
+
return fmt.Errorf("npm install failed in %s: %w\noutput: %s", dir, err, string(out))
|
|
14
|
+
}
|
|
15
|
+
return nil
|
|
16
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
package init
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"io/fs"
|
|
5
|
+
"os"
|
|
6
|
+
"os/exec"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"strings"
|
|
9
|
+
"testing"
|
|
10
|
+
"testing/fstest"
|
|
11
|
+
|
|
12
|
+
"github.com/opencode/savepoint/internal/testutil"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
func runInitPipeline(t *testing.T, dir string, force bool) string {
|
|
16
|
+
t.Helper()
|
|
17
|
+
|
|
18
|
+
if err := ValidateTarget(dir, force); err != nil {
|
|
19
|
+
t.Fatal(err)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
templates := fstest.MapFS{
|
|
23
|
+
".savepoint": &fstest.MapFile{Mode: fs.ModeDir | 0755},
|
|
24
|
+
".savepoint/config.yml": &fstest.MapFile{Data: []byte("key: value")},
|
|
25
|
+
".savepoint/Design.md": &fstest.MapFile{Data: []byte("# {{PROJECT_NAME}} Design")},
|
|
26
|
+
".savepoint/PRD.md": &fstest.MapFile{Data: []byte("PRD: {{PROJECT_NAME}}")},
|
|
27
|
+
".savepoint/router.md": &fstest.MapFile{Data: []byte("# Router")},
|
|
28
|
+
".savepoint/visual-identity.md": &fstest.MapFile{Data: []byte("# Visual Identity")},
|
|
29
|
+
"AGENTS.md": &fstest.MapFile{Data: []byte("# Agents Guide\n\nBuild: npm run build")},
|
|
30
|
+
"agent-skills/savepoint-audit/SKILL.md": &fstest.MapFile{Data: []byte("# Audit Skill")},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
projectName := ProjectNameFromDir(dir)
|
|
34
|
+
if err := Scaffold(templates, dir, projectName, force); err != nil {
|
|
35
|
+
t.Fatal(err)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
promptTemplates := fstest.MapFS{
|
|
39
|
+
"magic-prompt.prompt.md": &fstest.MapFile{
|
|
40
|
+
Data: []byte("<!-- AGENT: Read AGENTS.md -->\n\nProject: {{PROJECT_NAME}}\n\nStart by reading AGENTS.md"),
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
prompt, err := RenderMagicPrompt(promptTemplates, projectName)
|
|
45
|
+
if err != nil {
|
|
46
|
+
t.Fatal(err)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return prompt
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func TestIntegration_EmptyDirectory(t *testing.T) {
|
|
53
|
+
dir := t.TempDir()
|
|
54
|
+
prompt := runInitPipeline(t, dir, false)
|
|
55
|
+
projectName := filepath.Base(dir)
|
|
56
|
+
|
|
57
|
+
entries := []string{
|
|
58
|
+
".savepoint/config.yml",
|
|
59
|
+
".savepoint/Design.md",
|
|
60
|
+
".savepoint/PRD.md",
|
|
61
|
+
".savepoint/router.md",
|
|
62
|
+
".savepoint/visual-identity.md",
|
|
63
|
+
"AGENTS.md",
|
|
64
|
+
"agent-skills/savepoint-audit/SKILL.md",
|
|
65
|
+
}
|
|
66
|
+
for _, e := range entries {
|
|
67
|
+
if _, err := os.Stat(filepath.Join(dir, e)); err != nil {
|
|
68
|
+
t.Errorf("missing %s: %v", e, err)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
data, err := os.ReadFile(filepath.Join(dir, ".savepoint", "Design.md"))
|
|
73
|
+
if err != nil {
|
|
74
|
+
t.Fatal(err)
|
|
75
|
+
}
|
|
76
|
+
if !strings.Contains(string(data), projectName) {
|
|
77
|
+
t.Errorf("Design.md content = %q, want to contain %q", string(data), projectName)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if !strings.Contains(prompt, projectName) {
|
|
81
|
+
t.Errorf("prompt = %q, want to contain %q", prompt, projectName)
|
|
82
|
+
}
|
|
83
|
+
if !strings.Contains(prompt, "AGENT") {
|
|
84
|
+
t.Errorf("prompt = %q, want AGENT marker", prompt)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
result := CopyToClipboard(prompt)
|
|
88
|
+
if result.Status != ClipboardCopied &&
|
|
89
|
+
result.Status != ClipboardSkipped &&
|
|
90
|
+
result.Status != ClipboardFailed {
|
|
91
|
+
t.Errorf("unexpected clipboard status: %v", result.Status)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func TestIntegration_CompatibleProject(t *testing.T) {
|
|
96
|
+
dir := t.TempDir()
|
|
97
|
+
|
|
98
|
+
for _, name := range []string{"package.json", ".git", "README.md"} {
|
|
99
|
+
testutil.WriteFile(t, filepath.Join(dir, name), "")
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
prompt := runInitPipeline(t, dir, false)
|
|
103
|
+
|
|
104
|
+
if _, err := os.Stat(filepath.Join(dir, "package.json")); err != nil {
|
|
105
|
+
t.Errorf("package.json missing: %v", err)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if _, err := os.Stat(filepath.Join(dir, ".savepoint", "config.yml")); err != nil {
|
|
109
|
+
t.Errorf(".savepoint/config.yml missing: %v", err)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if !strings.Contains(prompt, "AGENT") {
|
|
113
|
+
t.Errorf("prompt should contain AGENT marker")
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
func TestIntegration_NoForceOnExistingSavepoint(t *testing.T) {
|
|
118
|
+
dir := t.TempDir()
|
|
119
|
+
|
|
120
|
+
savepointDir := filepath.Join(dir, ".savepoint")
|
|
121
|
+
if err := os.Mkdir(savepointDir, 0755); err != nil {
|
|
122
|
+
t.Fatal(err)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
err := ValidateTarget(dir, false)
|
|
126
|
+
if err == nil {
|
|
127
|
+
t.Fatal("expected error for existing .savepoint without --force")
|
|
128
|
+
}
|
|
129
|
+
if !strings.Contains(err.Error(), "already contains") {
|
|
130
|
+
t.Errorf("error = %q, want 'already contains'", err.Error())
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func TestIntegration_ForceOverwritesExistingSavepoint(t *testing.T) {
|
|
135
|
+
dir := t.TempDir()
|
|
136
|
+
|
|
137
|
+
savepointDir := filepath.Join(dir, ".savepoint")
|
|
138
|
+
if err := os.Mkdir(savepointDir, 0755); err != nil {
|
|
139
|
+
t.Fatal(err)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
prompt := runInitPipeline(t, dir, true)
|
|
143
|
+
|
|
144
|
+
for _, path := range []string{
|
|
145
|
+
".savepoint/config.yml", ".savepoint/Design.md",
|
|
146
|
+
".savepoint/PRD.md", ".savepoint/router.md",
|
|
147
|
+
} {
|
|
148
|
+
if _, err := os.Stat(filepath.Join(dir, path)); err != nil {
|
|
149
|
+
t.Errorf("expected %s to exist after --force: %v", path, err)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if !strings.Contains(prompt, "AGENT") {
|
|
154
|
+
t.Errorf("prompt should contain AGENT marker")
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
func TestIntegration_InstallDependencies(t *testing.T) {
|
|
159
|
+
if _, err := exec.LookPath("npm"); err != nil {
|
|
160
|
+
t.Skip("npm not found in PATH, skipping install test")
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
dir := t.TempDir()
|
|
164
|
+
|
|
165
|
+
packageJSON := filepath.Join(dir, "package.json")
|
|
166
|
+
testutil.WriteFile(t, packageJSON, `{"name":"test","version":"0.0.0"}`)
|
|
167
|
+
|
|
168
|
+
if err := ValidateTarget(dir, false); err != nil {
|
|
169
|
+
t.Fatal(err)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if err := InstallDependencies(dir); err != nil {
|
|
173
|
+
t.Fatalf("InstallDependencies() error = %v", err)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if _, err := os.Stat(filepath.Join(dir, "package-lock.json")); err != nil {
|
|
177
|
+
t.Errorf("package-lock.json not created after npm install: %v", err)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
func TestIntegration_MagicPromptInOutput(t *testing.T) {
|
|
182
|
+
dir := t.TempDir()
|
|
183
|
+
prompt := runInitPipeline(t, dir, false)
|
|
184
|
+
projectName := filepath.Base(dir)
|
|
185
|
+
|
|
186
|
+
expectedParts := []string{
|
|
187
|
+
projectName,
|
|
188
|
+
"AGENT",
|
|
189
|
+
"AGENTS.md",
|
|
190
|
+
"Project",
|
|
191
|
+
}
|
|
192
|
+
for _, part := range expectedParts {
|
|
193
|
+
if !strings.Contains(prompt, part) {
|
|
194
|
+
t.Errorf("prompt missing %q", part)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
package init
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"io/fs"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
func RenderMagicPrompt(templates fs.FS, projectName string) (string, error) {
|
|
9
|
+
content, err := fs.ReadFile(templates, "magic-prompt.prompt.md")
|
|
10
|
+
if err != nil {
|
|
11
|
+
return "", fmt.Errorf("read magic prompt template: %w", err)
|
|
12
|
+
}
|
|
13
|
+
return interpolate(string(content), projectName), nil
|
|
14
|
+
}
|