devrites 4.6.0 → 4.7.0
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/CHANGELOG.md +6 -0
- package/README.md +4 -3
- package/docs/adr/0029-v5-workspace-schema-and-native-migration.md +62 -0
- package/docs/adr/README.md +3 -2
- package/docs/architecture.md +1 -1
- package/docs/cli.md +2 -2
- package/docs/command-map.md +2 -2
- package/docs/engine/commands.md +9 -0
- package/docs/engine/state-schema.md +2 -2
- package/docs/engine/workspace-schema.md +9 -3
- package/docs/usage.md +1 -1
- package/engine/commands.go +23 -0
- package/engine/internal/fsutil/copy_test.go +193 -0
- package/engine/internal/gate/gate_test.go +7 -7
- package/engine/internal/gate/readiness_binding_test.go +8 -2
- package/engine/internal/install/apply.go +314 -0
- package/engine/internal/install/binary.go +255 -0
- package/engine/internal/install/install.go +0 -1596
- package/engine/internal/install/manifest.go +109 -0
- package/engine/internal/install/merge.go +468 -0
- package/engine/internal/install/preflight.go +234 -0
- package/engine/internal/install/uninstall.go +119 -0
- package/engine/internal/install/update.go +184 -0
- package/engine/internal/lib/candidate_test.go +3 -0
- package/engine/internal/lib/closeout.go +4 -0
- package/engine/internal/lib/closeout_test.go +3 -3
- package/engine/internal/lib/migrate.go +219 -0
- package/engine/internal/lib/migrate_test.go +154 -0
- package/engine/internal/lib/resolve.go +3 -0
- package/engine/internal/lib/resolve_remediation_test.go +1 -1
- package/engine/internal/lib/secretscan_test.go +1 -0
- package/engine/internal/lib/taskgraph_test.go +2 -2
- package/engine/internal/parallel/ops.go +67 -16
- package/engine/internal/parallel/ops_test.go +211 -0
- package/engine/internal/state/cmd/workflowmanifest/main.go +24 -13
- package/engine/internal/state/cmd/workflowmanifest/main_test.go +96 -0
- package/engine/internal/state/cursor.go +74 -0
- package/engine/internal/state/schema.go +3 -2
- package/engine/internal/state/state_test.go +2 -2
- package/engine/internal/state/workflow_manifest.json +1 -1
- package/engine/internal/state/workspaceschema.go +58 -0
- package/engine/internal/state/workspaceschema_test.go +67 -0
- package/engine/main.go +7 -31
- package/engine/root_routing_test.go +10 -2
- package/engine/testdata/fixtures/basic/devrites-root/work/auth-tokens/state.md +1 -0
- package/engine/testdata/fixtures/basic/devrites-root/work/search-ranking/state.md +1 -0
- package/engine/testdata/golden/TestParityResolve/answer/state.md.golden +1 -0
- package/engine/testdata/golden/TestParityResolve/drop/state.md.golden +1 -0
- package/engine/tests/parity_closeout_test.go +3 -0
- package/engine/tests/parity_resolve_test.go +1 -0
- package/pack/.claude/agents/devrites-code-reviewer.md +8 -0
- package/pack/.claude/agents/devrites-devex-reviewer.md +7 -0
- package/pack/.claude/agents/devrites-doubt-reviewer.md +7 -0
- package/pack/.claude/agents/devrites-evidence-scout.md +4 -0
- package/pack/.claude/agents/devrites-frontend-reviewer.md +8 -0
- package/pack/.claude/agents/devrites-performance-reviewer.md +7 -0
- package/pack/.claude/agents/devrites-plan-reviewer.md +8 -0
- package/pack/.claude/agents/devrites-security-auditor.md +7 -0
- package/pack/.claude/agents/devrites-simplifier-reviewer.md +7 -0
- package/pack/.claude/agents/devrites-spec-reviewer.md +7 -0
- package/pack/.claude/agents/devrites-strategy-reviewer.md +7 -0
- package/pack/.claude/agents/devrites-test-analyst.md +7 -0
- package/pack/.claude/skills/devrites-frontend-craft/reference/quality-standards.md +7 -2
- package/pack/.claude/skills/devrites-lib/reference/intent-map.md +7 -0
- package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +1 -1
- package/pack/.claude/skills/devrites-lib/reference/reply-contract.md +4 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/code-review.md +6 -1
- package/pack/.claude/skills/devrites-lib/reference/standards/coding-style.md +4 -3
- package/pack/.claude/skills/devrites-lib/reference/standards/context-hygiene.md +4 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/data-integrity.md +4 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/edge-case-trace.md +3 -1
- package/pack/.claude/skills/devrites-lib/reference/standards/elicitation.md +5 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/error-handling.md +3 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/integration-reliability.md +18 -6
- package/pack/.claude/skills/devrites-lib/reference/standards/patterns.md +16 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/repository-topology.md +6 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/security-checklist.md +3 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/security.md +9 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/skill-authoring.md +5 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/tooling.md +1 -1
- package/pack/.claude/skills/rite-learn/SKILL.md +4 -0
- package/pack/.claude/skills/rite-polish/SKILL.md +4 -0
- package/pack/generated/claude/agents/devrites-code-reviewer.md +8 -0
- package/pack/generated/claude/agents/devrites-devex-reviewer.md +7 -0
- package/pack/generated/claude/agents/devrites-doubt-reviewer.md +7 -0
- package/pack/generated/claude/agents/devrites-evidence-scout.md +4 -0
- package/pack/generated/claude/agents/devrites-frontend-reviewer.md +8 -0
- package/pack/generated/claude/agents/devrites-performance-reviewer.md +7 -0
- package/pack/generated/claude/agents/devrites-plan-reviewer.md +8 -0
- package/pack/generated/claude/agents/devrites-security-auditor.md +7 -0
- package/pack/generated/claude/agents/devrites-simplifier-reviewer.md +7 -0
- package/pack/generated/claude/agents/devrites-spec-reviewer.md +7 -0
- package/pack/generated/claude/agents/devrites-strategy-reviewer.md +7 -0
- package/pack/generated/claude/agents/devrites-test-analyst.md +7 -0
- package/pack/generated/claude/skills/devrites-frontend-craft/reference/quality-standards.md +7 -2
- package/pack/generated/claude/skills/devrites-lib/reference/intent-map.md +7 -0
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +1 -1
- package/pack/generated/claude/skills/devrites-lib/reference/reply-contract.md +4 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/code-review.md +6 -1
- package/pack/generated/claude/skills/devrites-lib/reference/standards/coding-style.md +4 -3
- package/pack/generated/claude/skills/devrites-lib/reference/standards/context-hygiene.md +4 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/data-integrity.md +4 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/edge-case-trace.md +3 -1
- package/pack/generated/claude/skills/devrites-lib/reference/standards/elicitation.md +5 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/error-handling.md +3 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/integration-reliability.md +18 -6
- package/pack/generated/claude/skills/devrites-lib/reference/standards/patterns.md +16 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/repository-topology.md +6 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/security-checklist.md +3 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/security.md +9 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/skill-authoring.md +5 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/tooling.md +1 -1
- package/pack/generated/claude/skills/rite-learn/SKILL.md +4 -0
- package/pack/generated/claude/skills/rite-polish/SKILL.md +4 -0
- package/pack/generated/codex/agents/devrites-code-reviewer.toml +8 -0
- package/pack/generated/codex/agents/devrites-devex-reviewer.toml +7 -0
- package/pack/generated/codex/agents/devrites-doubt-reviewer.toml +7 -0
- package/pack/generated/codex/agents/devrites-evidence-scout.toml +4 -0
- package/pack/generated/codex/agents/devrites-frontend-reviewer.toml +8 -0
- package/pack/generated/codex/agents/devrites-performance-reviewer.toml +7 -0
- package/pack/generated/codex/agents/devrites-plan-reviewer.toml +8 -0
- package/pack/generated/codex/agents/devrites-security-auditor.toml +7 -0
- package/pack/generated/codex/agents/devrites-simplifier-reviewer.toml +7 -0
- package/pack/generated/codex/agents/devrites-spec-reviewer.toml +7 -0
- package/pack/generated/codex/agents/devrites-strategy-reviewer.toml +7 -0
- package/pack/generated/codex/agents/devrites-test-analyst.toml +7 -0
- package/pack/generated/codex/skills/devrites-frontend-craft/reference/quality-standards.md +7 -2
- package/pack/generated/codex/skills/devrites-lib/reference/intent-map.md +7 -0
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +1 -1
- package/pack/generated/codex/skills/devrites-lib/reference/reply-contract.md +4 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/code-review.md +6 -1
- package/pack/generated/codex/skills/devrites-lib/reference/standards/coding-style.md +4 -3
- package/pack/generated/codex/skills/devrites-lib/reference/standards/context-hygiene.md +4 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/data-integrity.md +4 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/edge-case-trace.md +3 -1
- package/pack/generated/codex/skills/devrites-lib/reference/standards/elicitation.md +5 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/error-handling.md +3 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/integration-reliability.md +18 -6
- package/pack/generated/codex/skills/devrites-lib/reference/standards/patterns.md +16 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/repository-topology.md +6 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/security-checklist.md +3 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/security.md +9 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/skill-authoring.md +5 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/tooling.md +1 -1
- package/pack/generated/codex/skills/rite-learn/SKILL.md +4 -0
- package/pack/generated/codex/skills/rite-polish/SKILL.md +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
package install
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"crypto/sha256"
|
|
5
|
+
"encoding/hex"
|
|
6
|
+
"fmt"
|
|
7
|
+
"io"
|
|
8
|
+
"io/fs"
|
|
9
|
+
"os"
|
|
10
|
+
"path/filepath"
|
|
11
|
+
"sort"
|
|
12
|
+
"strings"
|
|
13
|
+
|
|
14
|
+
"github.com/devrites/devrites/internal/hostpack"
|
|
15
|
+
"github.com/devrites/devrites/internal/safepath"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
func (r *runner) preflightInstall() error {
|
|
19
|
+
desired, err := r.desiredInstallPaths()
|
|
20
|
+
if err != nil {
|
|
21
|
+
return fmt.Errorf("preflight install paths: %w", err)
|
|
22
|
+
}
|
|
23
|
+
var conflicts []string
|
|
24
|
+
for rel := range desired {
|
|
25
|
+
snapshot, err := r.rememberPath(rel)
|
|
26
|
+
if err != nil {
|
|
27
|
+
return err
|
|
28
|
+
}
|
|
29
|
+
record, managed := r.prev[rel]
|
|
30
|
+
if managed && !snapshot.missing {
|
|
31
|
+
if kind := managedConflict(record, snapshot); kind != "" && !r.opts.Force {
|
|
32
|
+
conflicts = append(conflicts, fmt.Sprintf("%s: %s", kind, rel))
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for rel, record := range r.prev {
|
|
37
|
+
if desired[rel] || hostpack.PreserveOnPrune(rel) {
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
snapshot, err := r.rememberPath(rel)
|
|
41
|
+
if err != nil {
|
|
42
|
+
return err
|
|
43
|
+
}
|
|
44
|
+
if !snapshot.missing {
|
|
45
|
+
if kind := managedConflict(record, snapshot); kind != "" && !r.opts.Force {
|
|
46
|
+
conflicts = append(conflicts, fmt.Sprintf("%s: %s", kind, rel))
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if merge, ok := hostpack.ManagedMergeForMarker(rel); ok {
|
|
50
|
+
if _, err := r.rememberPath(merge.TargetRel); err != nil {
|
|
51
|
+
return err
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for _, rel := range r.installMergeTargets() {
|
|
56
|
+
if _, err := r.rememberPath(rel); err != nil {
|
|
57
|
+
return err
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if _, err := r.rememberPath(ManifestName); err != nil {
|
|
61
|
+
return err
|
|
62
|
+
}
|
|
63
|
+
return managedConflictError(conflicts)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func (r *runner) preflightUninstall(entries []string) error {
|
|
67
|
+
var conflicts []string
|
|
68
|
+
for _, rel := range entries {
|
|
69
|
+
if merge, ok := hostpack.ManagedMergeForMarker(rel); ok {
|
|
70
|
+
if _, err := r.rememberPath(merge.TargetRel); err != nil {
|
|
71
|
+
return err
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if !hostpack.ShouldRemoveOnUninstall(rel, entries) {
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
snapshot, err := r.rememberPath(rel)
|
|
78
|
+
if err != nil {
|
|
79
|
+
return err
|
|
80
|
+
}
|
|
81
|
+
if snapshot.missing {
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
if kind := managedConflict(r.prev[rel], snapshot); kind != "" && !r.opts.Force {
|
|
85
|
+
conflicts = append(conflicts, fmt.Sprintf("%s: %s", kind, rel))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if _, err := r.rememberPath(ManifestName); err != nil {
|
|
89
|
+
return err
|
|
90
|
+
}
|
|
91
|
+
return managedConflictError(conflicts)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func (r *runner) desiredInstallPaths() (map[string]bool, error) {
|
|
95
|
+
out := map[string]bool{".devrites/README.md": true}
|
|
96
|
+
for _, tree := range hostpack.InstallTrees(r.opts.WithSkills, r.opts.WithAgents, r.opts.WithCodex) {
|
|
97
|
+
err := fs.WalkDir(r.payloadFS, tree.PayloadPrefix, func(path string, d fs.DirEntry, err error) error {
|
|
98
|
+
if err != nil {
|
|
99
|
+
return err
|
|
100
|
+
}
|
|
101
|
+
if d.IsDir() {
|
|
102
|
+
return nil
|
|
103
|
+
}
|
|
104
|
+
rel, err := filepath.Rel(tree.PayloadPrefix, path)
|
|
105
|
+
if err != nil {
|
|
106
|
+
return err
|
|
107
|
+
}
|
|
108
|
+
out[filepath.ToSlash(filepath.Join(tree.TargetPrefix, rel))] = true
|
|
109
|
+
return nil
|
|
110
|
+
})
|
|
111
|
+
if err != nil {
|
|
112
|
+
return nil, err
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if r.opts.WithSkills && r.opts.AliasMode == "all" {
|
|
116
|
+
for _, alias := range hostpack.Aliases {
|
|
117
|
+
for _, rel := range hostpack.AliasTargets(alias, r.opts.WithCodex) {
|
|
118
|
+
out[rel] = true
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if r.opts.WithSkills && r.opts.WithCodex {
|
|
123
|
+
out[hostpack.CodexAgentsMerge.MarkerRel] = true
|
|
124
|
+
out[hostpack.CodexConfigMerge.MarkerRel] = true
|
|
125
|
+
}
|
|
126
|
+
if r.opts.WithSkills {
|
|
127
|
+
out[hostpack.ClaudeSettingsMerge.MarkerRel] = true
|
|
128
|
+
}
|
|
129
|
+
return out, nil
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
func (r *runner) installMergeTargets() []string {
|
|
133
|
+
var out []string
|
|
134
|
+
if r.opts.WithSkills {
|
|
135
|
+
out = append(out, hostpack.ClaudeSettingsMerge.TargetRel)
|
|
136
|
+
if r.opts.WithCodex {
|
|
137
|
+
out = append(out, hostpack.CodexAgentsMerge.TargetRel, hostpack.CodexConfigMerge.TargetRel)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return out
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func (r *runner) rememberPath(rel string) (pathSnapshot, error) {
|
|
144
|
+
rel = filepath.ToSlash(rel)
|
|
145
|
+
if snapshot, ok := r.preflight[rel]; ok {
|
|
146
|
+
return snapshot, nil
|
|
147
|
+
}
|
|
148
|
+
snapshot, err := inspectManagedPath(r.target, rel)
|
|
149
|
+
if err != nil {
|
|
150
|
+
return pathSnapshot{}, err
|
|
151
|
+
}
|
|
152
|
+
r.preflight[rel] = snapshot
|
|
153
|
+
return snapshot, nil
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
func (r *runner) recheckPath(rel string) error {
|
|
157
|
+
rel = filepath.ToSlash(rel)
|
|
158
|
+
before, ok := r.preflight[rel]
|
|
159
|
+
if !ok {
|
|
160
|
+
return fmt.Errorf("path %s was not preflighted", rel)
|
|
161
|
+
}
|
|
162
|
+
now, err := inspectManagedPath(r.target, rel)
|
|
163
|
+
if err != nil {
|
|
164
|
+
return err
|
|
165
|
+
}
|
|
166
|
+
if now != before {
|
|
167
|
+
return fmt.Errorf("refusing to change %s: file changed after preflight; retry the operation", rel)
|
|
168
|
+
}
|
|
169
|
+
return nil
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
func inspectManagedPath(target, rel string) (pathSnapshot, error) {
|
|
173
|
+
native := filepath.FromSlash(rel)
|
|
174
|
+
if !filepath.IsLocal(native) {
|
|
175
|
+
return pathSnapshot{}, fmt.Errorf("refusing unsafe managed path %q", rel)
|
|
176
|
+
}
|
|
177
|
+
dest := filepath.Join(target, native)
|
|
178
|
+
if !safepath.WithinResolved(dest, target) {
|
|
179
|
+
return pathSnapshot{}, fmt.Errorf("refusing managed path outside target: %s", rel)
|
|
180
|
+
}
|
|
181
|
+
walk := target
|
|
182
|
+
parts := strings.Split(native, string(filepath.Separator))
|
|
183
|
+
for _, part := range parts {
|
|
184
|
+
walk = filepath.Join(walk, part)
|
|
185
|
+
info, err := os.Lstat(walk)
|
|
186
|
+
if os.IsNotExist(err) {
|
|
187
|
+
return pathSnapshot{missing: true}, nil
|
|
188
|
+
}
|
|
189
|
+
if err != nil {
|
|
190
|
+
return pathSnapshot{}, fmt.Errorf("inspect managed path %s: %w", rel, err)
|
|
191
|
+
}
|
|
192
|
+
if info.Mode()&os.ModeSymlink != 0 {
|
|
193
|
+
return pathSnapshot{}, fmt.Errorf("refusing symlink or junction in managed path: %s", rel)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
info, err := os.Lstat(dest)
|
|
197
|
+
if os.IsNotExist(err) {
|
|
198
|
+
return pathSnapshot{missing: true}, nil
|
|
199
|
+
}
|
|
200
|
+
if err != nil {
|
|
201
|
+
return pathSnapshot{}, fmt.Errorf("inspect managed path %s: %w", rel, err)
|
|
202
|
+
}
|
|
203
|
+
if !info.Mode().IsRegular() {
|
|
204
|
+
return pathSnapshot{}, fmt.Errorf("refusing non-regular managed path: %s", rel)
|
|
205
|
+
}
|
|
206
|
+
f, err := os.Open(dest)
|
|
207
|
+
if err != nil {
|
|
208
|
+
return pathSnapshot{}, fmt.Errorf("read managed path %s: %w", rel, err)
|
|
209
|
+
}
|
|
210
|
+
defer f.Close()
|
|
211
|
+
sum := sha256.New()
|
|
212
|
+
if _, err := io.Copy(sum, f); err != nil {
|
|
213
|
+
return pathSnapshot{}, fmt.Errorf("hash managed path %s: %w", rel, err)
|
|
214
|
+
}
|
|
215
|
+
return pathSnapshot{hash: "sha256:" + hex.EncodeToString(sum.Sum(nil))}, nil
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
func managedConflict(record managedRecord, snapshot pathSnapshot) string {
|
|
219
|
+
if record.Hash == "" {
|
|
220
|
+
return "legacy manifest entry (no hash)"
|
|
221
|
+
}
|
|
222
|
+
if record.Hash != snapshot.hash {
|
|
223
|
+
return "customized managed file"
|
|
224
|
+
}
|
|
225
|
+
return ""
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
func managedConflictError(conflicts []string) error {
|
|
229
|
+
if len(conflicts) == 0 {
|
|
230
|
+
return nil
|
|
231
|
+
}
|
|
232
|
+
sort.Strings(conflicts)
|
|
233
|
+
return fmt.Errorf("managed files differ from the install manifest:\n %s\nrerun with --force to replace or remove these files", strings.Join(conflicts, "\n "))
|
|
234
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
package install
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
|
|
8
|
+
"github.com/devrites/devrites/internal/hostpack"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
func (r *runner) uninstall() error {
|
|
12
|
+
mf := filepath.Join(r.target, ManifestName)
|
|
13
|
+
entries, err := readManifestList(mf)
|
|
14
|
+
if err != nil {
|
|
15
|
+
return err
|
|
16
|
+
}
|
|
17
|
+
if len(entries) == 0 {
|
|
18
|
+
return fmt.Errorf("no DevRites manifest at %s - nothing to uninstall", mf)
|
|
19
|
+
}
|
|
20
|
+
fmt.Fprintln(r.opts.Stdout, "DevRites uninstaller")
|
|
21
|
+
fmt.Fprintf(r.opts.Stdout, " target : %s\n", r.target)
|
|
22
|
+
fmt.Fprintf(r.opts.Stdout, " manifest: %s\n", mf)
|
|
23
|
+
if r.opts.DryRun {
|
|
24
|
+
fmt.Fprintln(r.opts.Stdout, " (dry run - no changes)")
|
|
25
|
+
}
|
|
26
|
+
fmt.Fprintln(r.opts.Stdout)
|
|
27
|
+
if err := r.preflightUninstall(entries); err != nil {
|
|
28
|
+
return err
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for _, rel := range entries {
|
|
32
|
+
merge, ok := hostpack.ManagedMergeForMarker(rel)
|
|
33
|
+
if !ok {
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
if r.opts.DryRun {
|
|
37
|
+
fmt.Fprintf(r.opts.Stdout, " [merge-remove] %s\n", merge.DryRun)
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
if err := r.recheckPath(rel); err != nil {
|
|
41
|
+
return err
|
|
42
|
+
}
|
|
43
|
+
if err := r.recheckPath(merge.TargetRel); err != nil {
|
|
44
|
+
return err
|
|
45
|
+
}
|
|
46
|
+
if merge.MarkerRel == hostpack.LegacyCodexHooksMerge.MarkerRel {
|
|
47
|
+
if err := stripHooksPath(filepath.Join(r.target, filepath.FromSlash(merge.TargetRel))); err != nil {
|
|
48
|
+
return fmt.Errorf("strip hooks from %s: %w", merge.TargetRel, err)
|
|
49
|
+
}
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
if merge.TargetRel == hostpack.ClaudeSettingsMerge.TargetRel {
|
|
53
|
+
if err := r.stripClaudeSettings(filepath.Join(r.target, filepath.FromSlash(merge.TargetRel)), true); err != nil {
|
|
54
|
+
return fmt.Errorf("strip DevRites settings from %s: %w", merge.TargetRel, err)
|
|
55
|
+
}
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
if err := stripMarkerPath(filepath.Join(r.target, filepath.FromSlash(merge.TargetRel)), merge.Begin, merge.End); err != nil {
|
|
59
|
+
return fmt.Errorf("strip marker block from %s: %w", merge.TargetRel, err)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
var dirs []string
|
|
64
|
+
for _, rel := range entries {
|
|
65
|
+
if !hostpack.ShouldRemoveOnUninstall(rel, entries) {
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
dest := filepath.Join(r.target, filepath.FromSlash(rel))
|
|
69
|
+
if exists(dest) {
|
|
70
|
+
action := "remove"
|
|
71
|
+
if managedConflict(r.prev[rel], r.preflight[rel]) != "" && r.opts.Force {
|
|
72
|
+
action = "remove(force-customized)"
|
|
73
|
+
}
|
|
74
|
+
if r.opts.DryRun {
|
|
75
|
+
fmt.Fprintf(r.opts.Stdout, " [%s] %s\n", action, rel)
|
|
76
|
+
} else {
|
|
77
|
+
if err := r.recheckPath(rel); err != nil {
|
|
78
|
+
return err
|
|
79
|
+
}
|
|
80
|
+
if err := os.Remove(dest); err != nil && !os.IsNotExist(err) {
|
|
81
|
+
return fmt.Errorf("remove %s: %w", rel, err)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
dirs = append(dirs, filepath.Dir(dest))
|
|
85
|
+
r.stats.removed++
|
|
86
|
+
} else {
|
|
87
|
+
r.stats.missing++
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if r.opts.DryRun {
|
|
91
|
+
fmt.Fprintf(r.opts.Stdout, " [remove] %s\n", ManifestName)
|
|
92
|
+
} else {
|
|
93
|
+
if err := r.recheckPath(ManifestName); err != nil {
|
|
94
|
+
return err
|
|
95
|
+
}
|
|
96
|
+
_ = os.Remove(mf)
|
|
97
|
+
dirs = append(dirs, filepath.Dir(mf))
|
|
98
|
+
for _, d := range dirs {
|
|
99
|
+
pruneEmptyDirs(d, r.target)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if err := r.removeBinary(); err != nil {
|
|
103
|
+
return fmt.Errorf("remove engine binary: %w", err)
|
|
104
|
+
}
|
|
105
|
+
fmt.Fprintln(r.opts.Stdout)
|
|
106
|
+
if r.opts.DryRun {
|
|
107
|
+
fmt.Fprintln(r.opts.Stdout, "DevRites uninstall plan complete (dry run)")
|
|
108
|
+
} else {
|
|
109
|
+
fmt.Fprintln(r.opts.Stdout, "DevRites uninstalled")
|
|
110
|
+
}
|
|
111
|
+
fmt.Fprintf(r.opts.Stdout, " removed: %d already-absent: %d\n", r.stats.removed, r.stats.missing)
|
|
112
|
+
if exists(filepath.Join(r.target, ".devrites", "work")) {
|
|
113
|
+
fmt.Fprintln(r.opts.Stdout, " kept .devrites/work/ (your feature data)")
|
|
114
|
+
}
|
|
115
|
+
if exists(filepath.Join(r.target, ".devrites", "ACTIVE")) {
|
|
116
|
+
fmt.Fprintln(r.opts.Stdout, " kept .devrites/ACTIVE (active-feature cursor)")
|
|
117
|
+
}
|
|
118
|
+
return nil
|
|
119
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
package install
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
"os/exec"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"strings"
|
|
10
|
+
"time"
|
|
11
|
+
|
|
12
|
+
"github.com/devrites/devrites/internal/hostpack"
|
|
13
|
+
"github.com/devrites/devrites/internal/release"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
func runUpdate(opts Options) error {
|
|
17
|
+
target, err := filepath.Abs(opts.Target)
|
|
18
|
+
if err != nil {
|
|
19
|
+
return fmt.Errorf("resolve target %s: %w", opts.Target, err)
|
|
20
|
+
}
|
|
21
|
+
mf := filepath.Join(target, ManifestName)
|
|
22
|
+
if !exists(mf) {
|
|
23
|
+
return fmt.Errorf("no DevRites install found at %s (missing %s)", target, ManifestName)
|
|
24
|
+
}
|
|
25
|
+
installed := manifestHeader(mf, "devrites-version")
|
|
26
|
+
if installed == "" {
|
|
27
|
+
installed = "unknown"
|
|
28
|
+
}
|
|
29
|
+
if opts.SourceDir == "" {
|
|
30
|
+
if opts.PayloadDir != "" {
|
|
31
|
+
return fmt.Errorf("local update with --payload-dir also requires --source-dir")
|
|
32
|
+
}
|
|
33
|
+
return runRemoteUpdate(opts, target, installed)
|
|
34
|
+
}
|
|
35
|
+
source, err := filepath.Abs(opts.SourceDir)
|
|
36
|
+
if err != nil {
|
|
37
|
+
return fmt.Errorf("resolve source %s: %w", opts.SourceDir, err)
|
|
38
|
+
}
|
|
39
|
+
candidate := installedVersion(source)
|
|
40
|
+
if candidate == "" || candidate == "unknown" {
|
|
41
|
+
return fmt.Errorf("derive update version from local source %s: package.json has no version", source)
|
|
42
|
+
}
|
|
43
|
+
candidate = strings.TrimPrefix(candidate, "v")
|
|
44
|
+
if os.Getenv("DEVRITES_UPDATE_HANDOFF") != "1" {
|
|
45
|
+
fmt.Fprintln(opts.Stdout, "DevRites update")
|
|
46
|
+
fmt.Fprintf(opts.Stdout, " project: %s\n", target)
|
|
47
|
+
fmt.Fprintf(opts.Stdout, " installed: %s\n", installed)
|
|
48
|
+
fmt.Fprintf(opts.Stdout, " candidate: %s\n", candidate)
|
|
49
|
+
}
|
|
50
|
+
if opts.UpdateCheck {
|
|
51
|
+
if installed == candidate || strings.TrimPrefix(installed, "v") == candidate {
|
|
52
|
+
fmt.Fprintln(opts.Stdout, "up to date.")
|
|
53
|
+
return nil
|
|
54
|
+
}
|
|
55
|
+
return fmt.Errorf("update available: %s -> %s", installed, candidate)
|
|
56
|
+
}
|
|
57
|
+
if !opts.Force && (installed == candidate || strings.TrimPrefix(installed, "v") == candidate) {
|
|
58
|
+
fmt.Fprintln(opts.Stdout, "already up to date (use --force to reinstall).")
|
|
59
|
+
return nil
|
|
60
|
+
}
|
|
61
|
+
if opts.PayloadDir == "" {
|
|
62
|
+
return fmt.Errorf("update requires --payload-dir with the locally prepared host payload")
|
|
63
|
+
}
|
|
64
|
+
payload, err := filepath.Abs(opts.PayloadDir)
|
|
65
|
+
if err != nil {
|
|
66
|
+
return fmt.Errorf("resolve payload %s: %w", opts.PayloadDir, err)
|
|
67
|
+
}
|
|
68
|
+
if err := hostpack.ValidatePayload(os.DirFS(payload), true); err != nil {
|
|
69
|
+
return fmt.Errorf("local update payload under %s is invalid: %w", payload, err)
|
|
70
|
+
}
|
|
71
|
+
installFlags := manifestHeader(mf, "devrites-flags")
|
|
72
|
+
next := DefaultOptions(ModeInstall)
|
|
73
|
+
next.Stdout = opts.Stdout
|
|
74
|
+
next.Stderr = opts.Stderr
|
|
75
|
+
if installFlags != "" {
|
|
76
|
+
if err := parseArgs(strings.Fields(installFlags), &next); err != nil {
|
|
77
|
+
return fmt.Errorf("parse manifest install flags: %w", err)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
next.Target = target
|
|
81
|
+
next.PayloadDir = payload
|
|
82
|
+
next.SourceDir = source
|
|
83
|
+
next.Force = opts.Force
|
|
84
|
+
if opts.DryRun {
|
|
85
|
+
next.DryRun = true
|
|
86
|
+
}
|
|
87
|
+
r, err := newRunner(next)
|
|
88
|
+
if err != nil {
|
|
89
|
+
return fmt.Errorf("prepare installer: %w", err)
|
|
90
|
+
}
|
|
91
|
+
r.requiredBinaryTag = "v" + candidate
|
|
92
|
+
if next.WithBinary && os.Getenv("DEVRITES_NO_BINARY") != "1" && !next.DryRun {
|
|
93
|
+
staged, cleanup, err := r.acquireBinary(r.requiredBinaryTag)
|
|
94
|
+
if err != nil {
|
|
95
|
+
return fmt.Errorf("prepare engine binary: %w", err)
|
|
96
|
+
}
|
|
97
|
+
defer cleanup()
|
|
98
|
+
r.preparedBinary = staged
|
|
99
|
+
}
|
|
100
|
+
return r.install()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
var (
|
|
104
|
+
resolveLatestRelease = release.Latest
|
|
105
|
+
acquireRelease = release.Acquire
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
func runRemoteUpdate(opts Options, target, installed string) error {
|
|
109
|
+
repository := os.Getenv("DEVRITES_REPO")
|
|
110
|
+
if repository == "" {
|
|
111
|
+
repository = release.DefaultRepository
|
|
112
|
+
}
|
|
113
|
+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
|
114
|
+
defer cancel()
|
|
115
|
+
tag, err := resolveLatestRelease(ctx, repository)
|
|
116
|
+
if err != nil {
|
|
117
|
+
return err
|
|
118
|
+
}
|
|
119
|
+
latest := strings.TrimPrefix(tag, "v")
|
|
120
|
+
fmt.Fprintln(opts.Stdout, "DevRites update")
|
|
121
|
+
fmt.Fprintf(opts.Stdout, " project: %s\n", target)
|
|
122
|
+
fmt.Fprintf(opts.Stdout, " installed: %s\n", installed)
|
|
123
|
+
fmt.Fprintf(opts.Stdout, " latest: %s\n", latest)
|
|
124
|
+
if opts.UpdateCheck {
|
|
125
|
+
if installed == latest || strings.TrimPrefix(installed, "v") == latest {
|
|
126
|
+
fmt.Fprintln(opts.Stdout, "up to date.")
|
|
127
|
+
return nil
|
|
128
|
+
}
|
|
129
|
+
return fmt.Errorf("update available: %s -> %s", installed, latest)
|
|
130
|
+
}
|
|
131
|
+
if !opts.Force && (installed == latest || strings.TrimPrefix(installed, "v") == latest) {
|
|
132
|
+
fmt.Fprintln(opts.Stdout, "already up to date (use --force to reinstall).")
|
|
133
|
+
return nil
|
|
134
|
+
}
|
|
135
|
+
candidate, cleanup, err := acquireRelease(ctx, repository, tag)
|
|
136
|
+
if err != nil {
|
|
137
|
+
return fmt.Errorf("acquire update %s: %w", tag, err)
|
|
138
|
+
}
|
|
139
|
+
defer cleanup()
|
|
140
|
+
fmt.Fprintf(opts.Stdout, " bundle: %s\n", candidate.BundleURL)
|
|
141
|
+
if _, err := verifyEngineBinary(candidate.EnginePath, tag, 30*time.Second); err != nil {
|
|
142
|
+
return fmt.Errorf("verify update engine: %w", err)
|
|
143
|
+
}
|
|
144
|
+
args := []string{
|
|
145
|
+
"update",
|
|
146
|
+
"--target", target,
|
|
147
|
+
"--source-dir", candidate.SourceDir,
|
|
148
|
+
"--payload-dir", candidate.PayloadDir,
|
|
149
|
+
}
|
|
150
|
+
if opts.DryRun {
|
|
151
|
+
args = append(args, "--dry-run")
|
|
152
|
+
}
|
|
153
|
+
if opts.Force {
|
|
154
|
+
args = append(args, "--force")
|
|
155
|
+
}
|
|
156
|
+
cmd := exec.CommandContext(ctx, candidate.EnginePath, args...)
|
|
157
|
+
cmd.Stdout = opts.Stdout
|
|
158
|
+
cmd.Stderr = opts.Stderr
|
|
159
|
+
cmd.Env = replaceEnv(os.Environ(), map[string]string{
|
|
160
|
+
"DEVRITES_ENGINE_CLI": candidate.EnginePath,
|
|
161
|
+
"DEVRITES_UPDATE_HANDOFF": "1",
|
|
162
|
+
})
|
|
163
|
+
if err := cmd.Run(); err != nil {
|
|
164
|
+
return fmt.Errorf("apply update %s: %w", tag, err)
|
|
165
|
+
}
|
|
166
|
+
return nil
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func replaceEnv(env []string, replacements map[string]string) []string {
|
|
170
|
+
out := make([]string, 0, len(env)+len(replacements))
|
|
171
|
+
for _, entry := range env {
|
|
172
|
+
name, _, found := strings.Cut(entry, "=")
|
|
173
|
+
if found {
|
|
174
|
+
if _, replace := replacements[name]; replace {
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
out = append(out, entry)
|
|
179
|
+
}
|
|
180
|
+
for name, value := range replacements {
|
|
181
|
+
out = append(out, name+"="+value)
|
|
182
|
+
}
|
|
183
|
+
return out
|
|
184
|
+
}
|
|
@@ -45,6 +45,9 @@ func writeCandidateTestWorkspace(t *testing.T, manifest string) (project, root s
|
|
|
45
45
|
t.Fatal(err)
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
if err := os.WriteFile(filepath.Join(workspace, "state.md"), []byte("| schema | 3 |\n"), 0o644); err != nil {
|
|
49
|
+
t.Fatal(err)
|
|
50
|
+
}
|
|
48
51
|
return project, root
|
|
49
52
|
}
|
|
50
53
|
|
|
@@ -37,6 +37,10 @@ func CloseOut(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
37
37
|
fmt.Fprintf(stderr, "state close: invalid workspace for %s: %v\n", slug, err)
|
|
38
38
|
return 1
|
|
39
39
|
}
|
|
40
|
+
if err := state.RequireWorkspaceSchema(dv, slug); err != nil {
|
|
41
|
+
fmt.Fprintf(stderr, "state close: %v\n", err)
|
|
42
|
+
return 3
|
|
43
|
+
}
|
|
40
44
|
archiveDir, err := devritespaths.ArchiveDirChecked(dv)
|
|
41
45
|
if err != nil {
|
|
42
46
|
fmt.Fprintf(stderr, "state close: invalid archive directory: %v\n", err)
|
|
@@ -28,7 +28,7 @@ func runCloseOut(t *testing.T, root string, args ...string) (int, string) {
|
|
|
28
28
|
|
|
29
29
|
func TestCloseOutRejectsRootOverrideArgument(t *testing.T) {
|
|
30
30
|
root := t.TempDir()
|
|
31
|
-
writeFile(t, filepath.Join(root, "work", "feat", "state.md"), "inside\n")
|
|
31
|
+
writeFile(t, filepath.Join(root, "work", "feat", "state.md"), "inside\n- Schema: 3\n")
|
|
32
32
|
|
|
33
33
|
code, out := runCloseOut(t, root, "feat", t.TempDir())
|
|
34
34
|
if code != 4 || !strings.Contains(out, "usage: devrites-engine state close <slug>") {
|
|
@@ -65,7 +65,7 @@ func TestCloseOutRejectsWorkspaceSymlinkEscape(t *testing.T) {
|
|
|
65
65
|
func TestCloseOutRejectsArchiveSymlinkEscape(t *testing.T) {
|
|
66
66
|
root := t.TempDir()
|
|
67
67
|
outside := t.TempDir()
|
|
68
|
-
writeFile(t, filepath.Join(root, "work", "feat", "state.md"), "inside\n")
|
|
68
|
+
writeFile(t, filepath.Join(root, "work", "feat", "state.md"), "inside\n- Schema: 3\n")
|
|
69
69
|
if err := os.Symlink(outside, filepath.Join(root, "archive")); err != nil {
|
|
70
70
|
t.Skipf("symlinks unavailable: %v", err)
|
|
71
71
|
}
|
|
@@ -92,7 +92,7 @@ func TestCloseOutRollsBackArchiveWhenActiveCannotBeCleared(t *testing.T) {
|
|
|
92
92
|
t.Skip("permission model differs on Windows")
|
|
93
93
|
}
|
|
94
94
|
root := t.TempDir()
|
|
95
|
-
writeFile(t, filepath.Join(root, "work", "feat", "state.md"), "inside\n")
|
|
95
|
+
writeFile(t, filepath.Join(root, "work", "feat", "state.md"), "inside\n- Schema: 3\n")
|
|
96
96
|
writeFile(t, filepath.Join(root, "ACTIVE"), "feat\n")
|
|
97
97
|
if err := os.MkdirAll(filepath.Join(root, "archive"), 0o755); err != nil {
|
|
98
98
|
t.Fatal(err)
|