devrites 4.5.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.
Files changed (191) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/NOTICE.md +0 -13
  3. package/README.md +4 -3
  4. package/docs/adr/0029-v5-workspace-schema-and-native-migration.md +62 -0
  5. package/docs/adr/README.md +3 -2
  6. package/docs/architecture.md +1 -1
  7. package/docs/cli.md +2 -2
  8. package/docs/command-map.md +2 -2
  9. package/docs/engine/commands.md +9 -0
  10. package/docs/engine/state-schema.md +2 -2
  11. package/docs/engine/workspace-schema.md +9 -3
  12. package/docs/usage.md +1 -1
  13. package/engine/commands.go +23 -0
  14. package/engine/internal/fsutil/copy_test.go +193 -0
  15. package/engine/internal/gate/gate_test.go +7 -7
  16. package/engine/internal/gate/readiness_binding_test.go +8 -2
  17. package/engine/internal/install/apply.go +314 -0
  18. package/engine/internal/install/binary.go +255 -0
  19. package/engine/internal/install/install.go +0 -1596
  20. package/engine/internal/install/manifest.go +109 -0
  21. package/engine/internal/install/merge.go +468 -0
  22. package/engine/internal/install/preflight.go +234 -0
  23. package/engine/internal/install/uninstall.go +119 -0
  24. package/engine/internal/install/update.go +184 -0
  25. package/engine/internal/lib/candidate_test.go +3 -0
  26. package/engine/internal/lib/closeout.go +4 -0
  27. package/engine/internal/lib/closeout_test.go +3 -3
  28. package/engine/internal/lib/migrate.go +219 -0
  29. package/engine/internal/lib/migrate_test.go +154 -0
  30. package/engine/internal/lib/resolve.go +3 -0
  31. package/engine/internal/lib/resolve_remediation_test.go +1 -1
  32. package/engine/internal/lib/secretscan_test.go +1 -0
  33. package/engine/internal/lib/taskgraph_test.go +2 -2
  34. package/engine/internal/parallel/ops.go +67 -16
  35. package/engine/internal/parallel/ops_test.go +211 -0
  36. package/engine/internal/state/cmd/workflowmanifest/main.go +24 -13
  37. package/engine/internal/state/cmd/workflowmanifest/main_test.go +96 -0
  38. package/engine/internal/state/cursor.go +74 -0
  39. package/engine/internal/state/schema.go +3 -2
  40. package/engine/internal/state/state_test.go +2 -2
  41. package/engine/internal/state/workflow_manifest.json +1 -1
  42. package/engine/internal/state/workspaceschema.go +58 -0
  43. package/engine/internal/state/workspaceschema_test.go +67 -0
  44. package/engine/main.go +7 -31
  45. package/engine/root_routing_test.go +10 -2
  46. package/engine/testdata/fixtures/basic/devrites-root/work/auth-tokens/state.md +1 -0
  47. package/engine/testdata/fixtures/basic/devrites-root/work/search-ranking/state.md +1 -0
  48. package/engine/testdata/golden/TestParityResolve/answer/state.md.golden +1 -0
  49. package/engine/testdata/golden/TestParityResolve/drop/state.md.golden +1 -0
  50. package/engine/tests/parity_closeout_test.go +3 -0
  51. package/engine/tests/parity_resolve_test.go +1 -0
  52. package/pack/.claude/agents/devrites-code-reviewer.md +8 -0
  53. package/pack/.claude/agents/devrites-devex-reviewer.md +7 -0
  54. package/pack/.claude/agents/devrites-doubt-reviewer.md +7 -0
  55. package/pack/.claude/agents/devrites-evidence-scout.md +4 -0
  56. package/pack/.claude/agents/devrites-frontend-reviewer.md +8 -0
  57. package/pack/.claude/agents/devrites-performance-reviewer.md +7 -0
  58. package/pack/.claude/agents/devrites-plan-reviewer.md +8 -0
  59. package/pack/.claude/agents/devrites-retrospector.md +2 -0
  60. package/pack/.claude/agents/devrites-security-auditor.md +7 -0
  61. package/pack/.claude/agents/devrites-simplifier-reviewer.md +7 -0
  62. package/pack/.claude/agents/devrites-spec-reviewer.md +7 -0
  63. package/pack/.claude/agents/devrites-strategy-reviewer.md +7 -0
  64. package/pack/.claude/agents/devrites-test-analyst.md +7 -0
  65. package/pack/.claude/skills/devrites-frontend-craft/SKILL.md +4 -7
  66. package/pack/.claude/skills/devrites-frontend-craft/reference/quality-standards.md +9 -4
  67. package/pack/.claude/skills/devrites-lib/reference/intent-map.md +7 -0
  68. package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +1 -1
  69. package/pack/.claude/skills/devrites-lib/reference/reply-contract.md +9 -7
  70. package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +1 -1
  71. package/pack/.claude/skills/devrites-lib/reference/standards/code-review.md +6 -1
  72. package/pack/.claude/skills/devrites-lib/reference/standards/coding-style.md +4 -3
  73. package/pack/.claude/skills/devrites-lib/reference/standards/context-hygiene.md +4 -0
  74. package/pack/.claude/skills/devrites-lib/reference/standards/core.md +3 -6
  75. package/pack/.claude/skills/devrites-lib/reference/standards/data-integrity.md +4 -0
  76. package/pack/.claude/skills/devrites-lib/reference/standards/development-workflow.md +8 -0
  77. package/pack/.claude/skills/devrites-lib/reference/standards/documentation.md +6 -0
  78. package/pack/.claude/skills/devrites-lib/reference/standards/edge-case-trace.md +3 -1
  79. package/pack/.claude/skills/devrites-lib/reference/standards/elicitation.md +5 -0
  80. package/pack/.claude/skills/devrites-lib/reference/standards/error-handling.md +14 -0
  81. package/pack/.claude/skills/devrites-lib/reference/standards/integration-reliability.md +18 -6
  82. package/pack/.claude/skills/devrites-lib/reference/standards/patterns.md +16 -0
  83. package/pack/.claude/skills/devrites-lib/reference/standards/performance.md +4 -0
  84. package/pack/.claude/skills/devrites-lib/reference/standards/repository-topology.md +6 -0
  85. package/pack/.claude/skills/devrites-lib/reference/standards/security-checklist.md +3 -0
  86. package/pack/.claude/skills/devrites-lib/reference/standards/security.md +10 -2
  87. package/pack/.claude/skills/devrites-lib/reference/standards/skill-authoring.md +8 -0
  88. package/pack/.claude/skills/devrites-lib/reference/standards/spec-grammar.md +5 -6
  89. package/pack/.claude/skills/devrites-lib/reference/standards/testing.md +2 -2
  90. package/pack/.claude/skills/devrites-lib/reference/standards/tooling.md +1 -1
  91. package/pack/.claude/skills/rite-define/reference/plan-template.md +5 -2
  92. package/pack/.claude/skills/rite-learn/SKILL.md +6 -0
  93. package/pack/.claude/skills/rite-polish/SKILL.md +4 -0
  94. package/pack/.claude/skills/rite-polish/reference/anti-ai-slop.md +12 -53
  95. package/pack/.claude/skills/rite-pr-feedback/SKILL.md +7 -2
  96. package/pack/.claude/skills/rite-spec/reference/spec-template.md +5 -4
  97. package/pack/.claude/skills/rite-status/SKILL.md +2 -0
  98. package/pack/generated/claude/agents/devrites-code-reviewer.md +8 -0
  99. package/pack/generated/claude/agents/devrites-devex-reviewer.md +7 -0
  100. package/pack/generated/claude/agents/devrites-doubt-reviewer.md +7 -0
  101. package/pack/generated/claude/agents/devrites-evidence-scout.md +4 -0
  102. package/pack/generated/claude/agents/devrites-frontend-reviewer.md +8 -0
  103. package/pack/generated/claude/agents/devrites-performance-reviewer.md +7 -0
  104. package/pack/generated/claude/agents/devrites-plan-reviewer.md +8 -0
  105. package/pack/generated/claude/agents/devrites-retrospector.md +2 -0
  106. package/pack/generated/claude/agents/devrites-security-auditor.md +7 -0
  107. package/pack/generated/claude/agents/devrites-simplifier-reviewer.md +7 -0
  108. package/pack/generated/claude/agents/devrites-spec-reviewer.md +7 -0
  109. package/pack/generated/claude/agents/devrites-strategy-reviewer.md +7 -0
  110. package/pack/generated/claude/agents/devrites-test-analyst.md +7 -0
  111. package/pack/generated/claude/skills/devrites-frontend-craft/SKILL.md +4 -7
  112. package/pack/generated/claude/skills/devrites-frontend-craft/reference/quality-standards.md +9 -4
  113. package/pack/generated/claude/skills/devrites-lib/reference/intent-map.md +7 -0
  114. package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +1 -1
  115. package/pack/generated/claude/skills/devrites-lib/reference/reply-contract.md +9 -7
  116. package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +1 -1
  117. package/pack/generated/claude/skills/devrites-lib/reference/standards/code-review.md +6 -1
  118. package/pack/generated/claude/skills/devrites-lib/reference/standards/coding-style.md +4 -3
  119. package/pack/generated/claude/skills/devrites-lib/reference/standards/context-hygiene.md +4 -0
  120. package/pack/generated/claude/skills/devrites-lib/reference/standards/core.md +3 -6
  121. package/pack/generated/claude/skills/devrites-lib/reference/standards/data-integrity.md +4 -0
  122. package/pack/generated/claude/skills/devrites-lib/reference/standards/development-workflow.md +8 -0
  123. package/pack/generated/claude/skills/devrites-lib/reference/standards/documentation.md +6 -0
  124. package/pack/generated/claude/skills/devrites-lib/reference/standards/edge-case-trace.md +3 -1
  125. package/pack/generated/claude/skills/devrites-lib/reference/standards/elicitation.md +5 -0
  126. package/pack/generated/claude/skills/devrites-lib/reference/standards/error-handling.md +14 -0
  127. package/pack/generated/claude/skills/devrites-lib/reference/standards/integration-reliability.md +18 -6
  128. package/pack/generated/claude/skills/devrites-lib/reference/standards/patterns.md +16 -0
  129. package/pack/generated/claude/skills/devrites-lib/reference/standards/performance.md +4 -0
  130. package/pack/generated/claude/skills/devrites-lib/reference/standards/repository-topology.md +6 -0
  131. package/pack/generated/claude/skills/devrites-lib/reference/standards/security-checklist.md +3 -0
  132. package/pack/generated/claude/skills/devrites-lib/reference/standards/security.md +10 -2
  133. package/pack/generated/claude/skills/devrites-lib/reference/standards/skill-authoring.md +8 -0
  134. package/pack/generated/claude/skills/devrites-lib/reference/standards/spec-grammar.md +5 -6
  135. package/pack/generated/claude/skills/devrites-lib/reference/standards/testing.md +2 -2
  136. package/pack/generated/claude/skills/devrites-lib/reference/standards/tooling.md +1 -1
  137. package/pack/generated/claude/skills/rite-define/reference/plan-template.md +5 -2
  138. package/pack/generated/claude/skills/rite-learn/SKILL.md +6 -0
  139. package/pack/generated/claude/skills/rite-polish/SKILL.md +4 -0
  140. package/pack/generated/claude/skills/rite-polish/reference/anti-ai-slop.md +12 -53
  141. package/pack/generated/claude/skills/rite-pr-feedback/SKILL.md +7 -2
  142. package/pack/generated/claude/skills/rite-spec/reference/spec-template.md +5 -4
  143. package/pack/generated/claude/skills/rite-status/SKILL.md +2 -0
  144. package/pack/generated/codex/agents/devrites-code-reviewer.toml +8 -0
  145. package/pack/generated/codex/agents/devrites-devex-reviewer.toml +7 -0
  146. package/pack/generated/codex/agents/devrites-doubt-reviewer.toml +7 -0
  147. package/pack/generated/codex/agents/devrites-evidence-scout.toml +4 -0
  148. package/pack/generated/codex/agents/devrites-frontend-reviewer.toml +8 -0
  149. package/pack/generated/codex/agents/devrites-performance-reviewer.toml +7 -0
  150. package/pack/generated/codex/agents/devrites-plan-reviewer.toml +8 -0
  151. package/pack/generated/codex/agents/devrites-retrospector.toml +2 -0
  152. package/pack/generated/codex/agents/devrites-security-auditor.toml +7 -0
  153. package/pack/generated/codex/agents/devrites-simplifier-reviewer.toml +7 -0
  154. package/pack/generated/codex/agents/devrites-spec-reviewer.toml +7 -0
  155. package/pack/generated/codex/agents/devrites-strategy-reviewer.toml +7 -0
  156. package/pack/generated/codex/agents/devrites-test-analyst.toml +7 -0
  157. package/pack/generated/codex/skills/devrites-frontend-craft/SKILL.md +4 -7
  158. package/pack/generated/codex/skills/devrites-frontend-craft/reference/quality-standards.md +9 -4
  159. package/pack/generated/codex/skills/devrites-lib/reference/intent-map.md +7 -0
  160. package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +1 -1
  161. package/pack/generated/codex/skills/devrites-lib/reference/reply-contract.md +9 -7
  162. package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +1 -1
  163. package/pack/generated/codex/skills/devrites-lib/reference/standards/code-review.md +6 -1
  164. package/pack/generated/codex/skills/devrites-lib/reference/standards/coding-style.md +4 -3
  165. package/pack/generated/codex/skills/devrites-lib/reference/standards/context-hygiene.md +4 -0
  166. package/pack/generated/codex/skills/devrites-lib/reference/standards/core.md +3 -6
  167. package/pack/generated/codex/skills/devrites-lib/reference/standards/data-integrity.md +4 -0
  168. package/pack/generated/codex/skills/devrites-lib/reference/standards/development-workflow.md +8 -0
  169. package/pack/generated/codex/skills/devrites-lib/reference/standards/documentation.md +6 -0
  170. package/pack/generated/codex/skills/devrites-lib/reference/standards/edge-case-trace.md +3 -1
  171. package/pack/generated/codex/skills/devrites-lib/reference/standards/elicitation.md +5 -0
  172. package/pack/generated/codex/skills/devrites-lib/reference/standards/error-handling.md +14 -0
  173. package/pack/generated/codex/skills/devrites-lib/reference/standards/integration-reliability.md +18 -6
  174. package/pack/generated/codex/skills/devrites-lib/reference/standards/patterns.md +16 -0
  175. package/pack/generated/codex/skills/devrites-lib/reference/standards/performance.md +4 -0
  176. package/pack/generated/codex/skills/devrites-lib/reference/standards/repository-topology.md +6 -0
  177. package/pack/generated/codex/skills/devrites-lib/reference/standards/security-checklist.md +3 -0
  178. package/pack/generated/codex/skills/devrites-lib/reference/standards/security.md +10 -2
  179. package/pack/generated/codex/skills/devrites-lib/reference/standards/skill-authoring.md +8 -0
  180. package/pack/generated/codex/skills/devrites-lib/reference/standards/spec-grammar.md +5 -6
  181. package/pack/generated/codex/skills/devrites-lib/reference/standards/testing.md +2 -2
  182. package/pack/generated/codex/skills/devrites-lib/reference/standards/tooling.md +1 -1
  183. package/pack/generated/codex/skills/rite-define/reference/plan-template.md +5 -2
  184. package/pack/generated/codex/skills/rite-learn/SKILL.md +6 -0
  185. package/pack/generated/codex/skills/rite-polish/SKILL.md +4 -0
  186. package/pack/generated/codex/skills/rite-polish/reference/anti-ai-slop.md +12 -53
  187. package/pack/generated/codex/skills/rite-pr-feedback/SKILL.md +7 -2
  188. package/pack/generated/codex/skills/rite-spec/reference/spec-template.md +5 -4
  189. package/pack/generated/codex/skills/rite-status/SKILL.md +2 -0
  190. package/package.json +1 -1
  191. package/docs/markdown-instruction-upgrade-2026-08-27.md +0 -127
@@ -0,0 +1,314 @@
1
+ package install
2
+
3
+ import (
4
+ "crypto/sha256"
5
+ "fmt"
6
+ "io/fs"
7
+ "os"
8
+ "path/filepath"
9
+ "slices"
10
+ "sort"
11
+ "strings"
12
+ "time"
13
+
14
+ "github.com/devrites/devrites/internal/fsutil"
15
+ "github.com/devrites/devrites/internal/hostpack"
16
+ )
17
+
18
+ func (r *runner) install() error {
19
+ fmt.Fprintln(r.opts.Stdout, "DevRites installer")
20
+ fmt.Fprintf(r.opts.Stdout, " target : %s\n", r.target)
21
+ fmt.Fprintf(r.opts.Stdout, " payload: %s\n", r.payload)
22
+ fmt.Fprintf(r.opts.Stdout, " skills : %s\n", yesno(r.opts.WithSkills))
23
+ fmt.Fprintln(r.opts.Stdout, " standards: ship inside the devrites-lib skill")
24
+ fmt.Fprintf(r.opts.Stdout, " agents : %s\n", yesno(r.opts.WithAgents))
25
+ fmt.Fprintf(r.opts.Stdout, " codex : %s\n", yesno(r.opts.WithCodex))
26
+ fmt.Fprintf(r.opts.Stdout, " aliases: %s\n", r.opts.AliasMode)
27
+ if r.opts.DryRun {
28
+ fmt.Fprintln(r.opts.Stdout, " (dry run - no changes will be made)")
29
+ }
30
+ fmt.Fprintln(r.opts.Stdout)
31
+ if r.preparedBinary != "" {
32
+ if _, err := verifyEngineBinary(r.preparedBinary, r.binaryTag(), 30*time.Second); err != nil {
33
+ return fmt.Errorf("verify staged engine binary %s: %w", r.preparedBinary, err)
34
+ }
35
+ }
36
+ if err := r.preflightInstall(); err != nil {
37
+ return err
38
+ }
39
+
40
+ for _, tree := range hostpack.InstallTrees(r.opts.WithSkills, r.opts.WithAgents, r.opts.WithCodex) {
41
+ if err := r.installTree(tree.PayloadPrefix, tree.TargetPrefix); err != nil {
42
+ return fmt.Errorf("install tree %s: %w", tree.TargetPrefix, err)
43
+ }
44
+ }
45
+ if r.opts.WithSkills && r.opts.AliasMode == "all" {
46
+ for _, alias := range hostpack.Aliases {
47
+ data, err := hostpack.RenderAliasSkill(alias)
48
+ if err != nil {
49
+ return fmt.Errorf("render alias skill %s: %w", alias.Name, err)
50
+ }
51
+ for _, rel := range hostpack.AliasTargets(alias, r.opts.WithCodex) {
52
+ if err := r.installData(data, rel); err != nil {
53
+ return fmt.Errorf("install alias: %w", err)
54
+ }
55
+ }
56
+ }
57
+ }
58
+ if r.opts.WithSkills && r.opts.WithCodex {
59
+ if err := r.mergeMarkerFile(hostpack.CodexAgentsMerge); err != nil {
60
+ return fmt.Errorf("merge %s: %w", hostpack.CodexAgentsMerge.TargetRel, err)
61
+ }
62
+ if err := r.mergeCodexConfig(); err != nil {
63
+ return fmt.Errorf("merge %s: %w", hostpack.CodexConfigMerge.TargetRel, err)
64
+ }
65
+ }
66
+ if r.opts.WithSkills {
67
+ if err := r.seedClaudeSettings(); err != nil {
68
+ return fmt.Errorf("seed claude settings: %w", err)
69
+ }
70
+ }
71
+ if err := r.seedDevrites(); err != nil {
72
+ return fmt.Errorf("seed .devrites: %w", err)
73
+ }
74
+ if err := r.pruneDropped(); err != nil {
75
+ return fmt.Errorf("prune dropped files: %w", err)
76
+ }
77
+ if !r.opts.DryRun {
78
+ if err := r.writeManifest(); err != nil {
79
+ return fmt.Errorf("write manifest: %w", err)
80
+ }
81
+ }
82
+ if err := r.installBinary(); err != nil {
83
+ return fmt.Errorf("install engine binary: %w", err)
84
+ }
85
+
86
+ fmt.Fprintln(r.opts.Stdout)
87
+ if r.opts.DryRun {
88
+ fmt.Fprintln(r.opts.Stdout, "DevRites install plan complete (dry run)")
89
+ } else {
90
+ fmt.Fprintln(r.opts.Stdout, "DevRites installed")
91
+ }
92
+ fmt.Fprintf(r.opts.Stdout, " installed: %d overwritten: %d skipped(conflict): %d pruned: %d\n", r.stats.installed, r.stats.overwrote, r.stats.skipped, r.stats.pruned)
93
+ if !r.opts.DryRun && r.opts.WithSkills {
94
+ if r.opts.WithCodex {
95
+ fmt.Fprintln(r.opts.Stdout, "Next: reopen the project, then run /rite (Claude) or $rite (Codex).")
96
+ } else {
97
+ fmt.Fprintln(r.opts.Stdout, "Next: reopen the project, then run /rite.")
98
+ }
99
+ }
100
+ return nil
101
+ }
102
+
103
+ func (r *runner) installTree(srcPrefix, relPrefix string) error {
104
+ return fs.WalkDir(r.payloadFS, srcPrefix, func(path string, d fs.DirEntry, err error) error {
105
+ if err != nil {
106
+ return fmt.Errorf("walk payload %s: %w", path, err)
107
+ }
108
+ if d.IsDir() {
109
+ return nil
110
+ }
111
+ rel, err := filepath.Rel(srcPrefix, path)
112
+ if err != nil {
113
+ return fmt.Errorf("resolve relative path for %s: %w", path, err)
114
+ }
115
+ return r.installFile(path, filepath.ToSlash(filepath.Join(relPrefix, rel)))
116
+ })
117
+ }
118
+
119
+ func (r *runner) installFile(src, rel string) error {
120
+ data, err := fs.ReadFile(r.payloadFS, src)
121
+ if err != nil {
122
+ return fmt.Errorf("read payload %s: %w", src, err)
123
+ }
124
+ return r.installData(data, rel)
125
+ }
126
+
127
+ func (r *runner) installData(data []byte, rel string) error {
128
+ dest := filepath.Join(r.target, filepath.FromSlash(rel))
129
+ action := "install"
130
+ if exists(dest) {
131
+ record, managed := r.prev[rel]
132
+ switch {
133
+ case managed && managedConflict(record, r.preflight[filepath.ToSlash(rel)]) != "" && r.opts.Force:
134
+ action = "overwrite(force-customized)"
135
+ case managed:
136
+ action = "overwrite"
137
+ case r.opts.Force:
138
+ action = "overwrite(force)"
139
+ default:
140
+ if r.opts.DryRun {
141
+ fmt.Fprintf(r.opts.Stdout, " [skip] %s (exists, not DevRites-managed)\n", rel)
142
+ } else {
143
+ fmt.Fprintf(r.opts.Stderr, "warning: skip %s (exists, not DevRites-managed; use --force to overwrite)\n", rel)
144
+ }
145
+ r.stats.skipped++
146
+ return nil
147
+ }
148
+ }
149
+ if r.opts.DryRun {
150
+ fmt.Fprintf(r.opts.Stdout, " [%s] %s\n", action, rel)
151
+ } else {
152
+ if err := r.recheckPath(rel); err != nil {
153
+ return err
154
+ }
155
+ if err := fsutil.WriteFileAtomic(dest, data, 0o644); err != nil {
156
+ return fmt.Errorf("cannot write %s: %w", rel, err)
157
+ }
158
+ }
159
+ r.addManifest(rel)
160
+ r.addInstallRecord(rel, data)
161
+ if action == "install" {
162
+ r.stats.installed++
163
+ } else {
164
+ r.stats.overwrote++
165
+ }
166
+ return nil
167
+ }
168
+
169
+ func (r *runner) addManifest(rel string) {
170
+ r.manifest = append(r.manifest, filepath.ToSlash(rel))
171
+ }
172
+
173
+ func (r *runner) addInstallRecord(rel string, data []byte) {
174
+ if r.records == nil {
175
+ r.records = map[string]string{}
176
+ }
177
+ sum := sha256.Sum256(data)
178
+ r.records[filepath.ToSlash(rel)] = fmt.Sprintf("sha256:%x", sum[:])
179
+ }
180
+
181
+ func (r *runner) installMarker(rel, text string) error {
182
+ return r.installData([]byte(text+"\n"), rel)
183
+ }
184
+
185
+ func (r *runner) seedDevrites() error {
186
+ readme, err := hostpack.RenderDevritesReadme()
187
+ if err != nil {
188
+ return fmt.Errorf("render readme: %w", err)
189
+ }
190
+ if err := r.installData(readme, ".devrites/README.md"); err != nil {
191
+ return fmt.Errorf("install readme: %w", err)
192
+ }
193
+ active := filepath.Join(r.target, ".devrites", "ACTIVE")
194
+ if exists(active) {
195
+ return nil
196
+ }
197
+ if r.opts.DryRun {
198
+ fmt.Fprintln(r.opts.Stdout, " [seed] .devrites/ACTIVE (runtime state - preserved on uninstall)")
199
+ return nil
200
+ }
201
+ if err := os.MkdirAll(filepath.Dir(active), 0o755); err != nil {
202
+ return fmt.Errorf("create %s: %w", filepath.Dir(active), err)
203
+ }
204
+ f, err := os.OpenFile(active, os.O_CREATE|os.O_EXCL, 0o644)
205
+ if err != nil {
206
+ if os.IsExist(err) {
207
+ return nil
208
+ }
209
+ return fmt.Errorf("create %s: %w", active, err)
210
+ }
211
+ return f.Close()
212
+ }
213
+
214
+ func (r *runner) pruneDropped() error {
215
+ if len(r.prev) == 0 {
216
+ return nil
217
+ }
218
+ next := map[string]bool{}
219
+ for _, rel := range r.manifest {
220
+ next[rel] = true
221
+ }
222
+ keys := make([]string, 0, len(r.prev))
223
+ for rel := range r.prev {
224
+ keys = append(keys, rel)
225
+ }
226
+ sort.Strings(keys)
227
+ for _, rel := range keys {
228
+ if next[rel] || hostpack.PreserveOnPrune(rel) {
229
+ continue
230
+ }
231
+ dead := filepath.Join(r.target, filepath.FromSlash(rel))
232
+ if !exists(dead) {
233
+ continue
234
+ }
235
+ action := "prune"
236
+ if managedConflict(r.prev[rel], r.preflight[rel]) != "" && r.opts.Force {
237
+ action = "prune(force-customized)"
238
+ }
239
+ if r.opts.DryRun {
240
+ fmt.Fprintf(r.opts.Stdout, " [%s] %s (dropped from pack)\n", action, rel)
241
+ } else {
242
+ if err := r.recheckPath(rel); err != nil {
243
+ return err
244
+ }
245
+ if merge, ok := hostpack.ManagedMergeForMarker(rel); ok {
246
+ if err := r.recheckPath(merge.TargetRel); err != nil {
247
+ return err
248
+ }
249
+ if merge.MarkerRel == hostpack.LegacyCodexHooksMerge.MarkerRel {
250
+ if err := stripHooksPath(filepath.Join(r.target, filepath.FromSlash(merge.TargetRel))); err != nil {
251
+ return fmt.Errorf("strip hooks from %s: %w", merge.TargetRel, err)
252
+ }
253
+ } else if merge.TargetRel == hostpack.ClaudeSettingsMerge.TargetRel {
254
+ _ = r.stripClaudeSettings(filepath.Join(r.target, filepath.FromSlash(merge.TargetRel)), true)
255
+ } else {
256
+ _ = stripMarkerPath(filepath.Join(r.target, filepath.FromSlash(merge.TargetRel)), merge.Begin, merge.End)
257
+ }
258
+ }
259
+ if err := os.Remove(dead); err != nil && !os.IsNotExist(err) {
260
+ return fmt.Errorf("remove %s: %w", rel, err)
261
+ }
262
+ pruneEmptyDirs(filepath.Dir(dead), r.target)
263
+ fmt.Fprintf(r.opts.Stdout, " [%s] %s\n", action, rel)
264
+ }
265
+ r.stats.pruned++
266
+ }
267
+ return nil
268
+ }
269
+
270
+ func (r *runner) writeManifest() error {
271
+ sort.Strings(r.manifest)
272
+ r.manifest = slices.Compact(r.manifest)
273
+ var b strings.Builder
274
+ b.WriteString("# DevRites install manifest - do not edit by hand.\n")
275
+ b.WriteString("# Generated " + time.Now().UTC().Format(time.RFC3339) + ". Uninstall removes exactly these paths.\n")
276
+ b.WriteString("# devrites-version: " + installedVersion(r.source) + "\n")
277
+ b.WriteString("# devrites-flags: " + r.flagsString() + "\n")
278
+ b.WriteString("# managed-records: source=npx payload=pack/generated format=rel sha256\n")
279
+ for _, rel := range r.manifest {
280
+ if hash := r.records[rel]; hash != "" {
281
+ b.WriteString("# managed: " + rel + " " + hash + "\n")
282
+ }
283
+ }
284
+ for _, rel := range r.manifest {
285
+ b.WriteString(rel + "\n")
286
+ }
287
+ if err := r.recheckPath(ManifestName); err != nil {
288
+ return err
289
+ }
290
+ return fsutil.WriteFileAtomic(filepath.Join(r.target, ManifestName), []byte(b.String()), 0o644)
291
+ }
292
+
293
+ func (r *runner) flagsString() string {
294
+ var flags []string
295
+ if !r.opts.WithSkills {
296
+ flags = append(flags, "--no-skills")
297
+ }
298
+ if !r.opts.WithAgents {
299
+ flags = append(flags, "--no-agents")
300
+ }
301
+ if !r.opts.WithCodex {
302
+ flags = append(flags, "--no-codex")
303
+ }
304
+ if !r.opts.WithBinary {
305
+ flags = append(flags, "--no-binary")
306
+ }
307
+ if r.opts.AliasMode == "off" {
308
+ flags = append(flags, "--no-short-aliases")
309
+ }
310
+ if r.opts.AliasMode == "all" {
311
+ flags = append(flags, "--short-aliases=all")
312
+ }
313
+ return strings.Join(flags, " ")
314
+ }
@@ -0,0 +1,255 @@
1
+ package install
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+ "io/fs"
8
+ "os"
9
+ "os/exec"
10
+ "path/filepath"
11
+ "runtime"
12
+ "strings"
13
+ "time"
14
+
15
+ "github.com/devrites/devrites/internal/fsutil"
16
+ )
17
+
18
+ func (r *runner) installBinary() error {
19
+ if !r.opts.WithBinary || os.Getenv("DEVRITES_NO_BINARY") == "1" {
20
+ fmt.Fprintln(r.opts.Stdout, " engine binary: skipped (--no-binary).")
21
+ return nil
22
+ }
23
+ if r.opts.DryRun {
24
+ fmt.Fprintln(r.opts.Stdout, " would install the global devrites-engine control-plane binary")
25
+ return nil
26
+ }
27
+ tag := r.binaryTag()
28
+ incoming := strings.TrimPrefix(tag, "v")
29
+ dest := binaryDest()
30
+ if exists(dest) {
31
+ if ev := engineVersion(dest); semverLike(ev) && semverLike(incoming) && verGT(ev, incoming) {
32
+ fmt.Fprintf(r.opts.Stderr, "warning: engine binary: installed %s is newer than %s - refusing to downgrade (kept).\n", ev, tag)
33
+ return nil
34
+ }
35
+ }
36
+ staged, cleanup := r.preparedBinary, func() {}
37
+ if staged == "" {
38
+ var err error
39
+ staged, cleanup, err = r.acquireBinary(tag)
40
+ if err != nil {
41
+ return r.binaryInstallFailure(err)
42
+ }
43
+ }
44
+ defer cleanup()
45
+ if _, err := verifyEngineBinary(staged, tag, 30*time.Second); err != nil {
46
+ return fmt.Errorf("verify staged engine binary %s: %w", staged, err)
47
+ }
48
+ if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
49
+ return err
50
+ }
51
+ data, err := os.ReadFile(staged)
52
+ if err != nil {
53
+ return err
54
+ }
55
+ backup, oldMode, hadOld, err := backupBinary(dest)
56
+ if err != nil {
57
+ return err
58
+ }
59
+ if backup != "" {
60
+ defer os.Remove(backup)
61
+ }
62
+ if err := fsutil.WriteFileAtomic(dest, data, 0o755); err != nil {
63
+ if restoreErr := restoreBinary(dest, backup, oldMode, hadOld); restoreErr != nil {
64
+ return fmt.Errorf("install binary: %v; restore previous binary: %w", err, restoreErr)
65
+ }
66
+ return err
67
+ }
68
+ _ = os.Chmod(dest, 0o755)
69
+ if _, err := verifyEngineBinary(dest, tag, 30*time.Second); err != nil {
70
+ if restoreErr := restoreBinary(dest, backup, oldMode, hadOld); restoreErr != nil {
71
+ return fmt.Errorf("verify installed engine binary: %v; restore previous binary: %w", err, restoreErr)
72
+ }
73
+ if hadOld {
74
+ return fmt.Errorf("verify installed engine binary: %w (previous binary restored)", err)
75
+ }
76
+ return fmt.Errorf("verify installed engine binary: %w (bad binary removed)", err)
77
+ }
78
+ fmt.Fprintf(r.opts.Stdout, " engine binary: installed %s\n", dest)
79
+ return nil
80
+ }
81
+
82
+ func (r *runner) binaryTag() string {
83
+ if r.requiredBinaryTag != "" {
84
+ return r.requiredBinaryTag
85
+ }
86
+ if tag := os.Getenv("DEVRITES_REF"); semverLike(tag) {
87
+ return tag
88
+ }
89
+ return "v" + strings.TrimPrefix(installedVersion(r.source), "v")
90
+ }
91
+
92
+ func backupBinary(dest string) (string, fs.FileMode, bool, error) {
93
+ info, err := os.Lstat(dest)
94
+ if os.IsNotExist(err) {
95
+ return "", 0, false, nil
96
+ }
97
+ if err != nil {
98
+ return "", 0, false, fmt.Errorf("inspect existing engine binary: %w", err)
99
+ }
100
+ if !info.Mode().IsRegular() {
101
+ return "", 0, false, fmt.Errorf("refusing to replace non-regular engine binary: %s", dest)
102
+ }
103
+ data, err := os.ReadFile(dest)
104
+ if err != nil {
105
+ return "", 0, false, fmt.Errorf("read existing engine binary: %w", err)
106
+ }
107
+ f, err := os.CreateTemp(filepath.Dir(dest), "."+filepath.Base(dest)+".backup-*")
108
+ if err != nil {
109
+ return "", 0, false, fmt.Errorf("create engine binary backup: %w", err)
110
+ }
111
+ backup := f.Name()
112
+ if err := f.Close(); err != nil {
113
+ _ = os.Remove(backup)
114
+ return "", 0, false, fmt.Errorf("create engine binary backup: %w", err)
115
+ }
116
+ if err := fsutil.WriteFileAtomic(backup, data, info.Mode().Perm()); err != nil {
117
+ _ = os.Remove(backup)
118
+ return "", 0, false, fmt.Errorf("write engine binary backup: %w", err)
119
+ }
120
+ return backup, info.Mode().Perm(), true, nil
121
+ }
122
+
123
+ func restoreBinary(dest, backup string, mode fs.FileMode, hadOld bool) error {
124
+ if !hadOld {
125
+ if err := os.Remove(dest); err != nil && !os.IsNotExist(err) {
126
+ return err
127
+ }
128
+ return nil
129
+ }
130
+ data, err := os.ReadFile(backup)
131
+ if err != nil {
132
+ return err
133
+ }
134
+ return fsutil.WriteFileAtomic(dest, data, mode)
135
+ }
136
+
137
+ func (r *runner) binaryInstallFailure(err error) error {
138
+ if r.preparedBinary != "" {
139
+ return err
140
+ }
141
+ if os.Getenv("DEVRITES_ENGINE_CLI") != "" {
142
+ return err
143
+ }
144
+ fmt.Fprintf(r.opts.Stderr, "warning: engine binary: %v; continuing without it.\n", err)
145
+ return nil
146
+ }
147
+
148
+ func (r *runner) acquireBinary(tag string) (string, func(), error) {
149
+ path := os.Getenv("DEVRITES_ENGINE_CLI")
150
+ if path == "" {
151
+ return "", func() {}, fmt.Errorf("no DEVRITES_ENGINE_CLI handoff")
152
+ }
153
+ if !exists(path) {
154
+ return "", func() {}, fmt.Errorf("DEVRITES_ENGINE_CLI points to a missing binary: %s", path)
155
+ }
156
+ if _, err := verifyEngineBinary(path, tag, 30*time.Second); err != nil {
157
+ return "", func() {}, fmt.Errorf("DEVRITES_ENGINE_CLI is incompatible: %w", err)
158
+ }
159
+ return path, func() {}, nil
160
+ }
161
+
162
+ func (r *runner) removeBinary() error {
163
+ if r.opts.KeepBinary {
164
+ fmt.Fprintln(r.opts.Stdout, " kept the global devrites-engine binary (--keep-binary).")
165
+ return nil
166
+ }
167
+ for _, p := range binaryCandidates() {
168
+ if p == "" || !exists(p) {
169
+ continue
170
+ }
171
+ if r.opts.DryRun {
172
+ fmt.Fprintf(r.opts.Stdout, " [remove] %s (global engine binary)\n", p)
173
+ continue
174
+ }
175
+ if err := os.Remove(p); err == nil {
176
+ fmt.Fprintf(r.opts.Stdout, " [remove] %s\n", p)
177
+ return nil
178
+ }
179
+ }
180
+ return nil
181
+ }
182
+
183
+ func binaryDest() string {
184
+ if dir := os.Getenv("DEVRITES_BIN_DIR"); dir != "" {
185
+ return filepath.Join(dir, engineBinaryName())
186
+ }
187
+ if home, err := os.UserHomeDir(); err == nil {
188
+ return filepath.Join(home, ".local", "bin", engineBinaryName())
189
+ }
190
+ return filepath.Join("/usr/local/bin", engineBinaryName())
191
+ }
192
+
193
+ func engineBinaryName() string {
194
+ if runtime.GOOS == "windows" {
195
+ return "devrites-engine.exe"
196
+ }
197
+ return "devrites-engine"
198
+ }
199
+
200
+ func binaryCandidates() []string {
201
+ candidates := []string{}
202
+ if dir := os.Getenv("DEVRITES_BIN_DIR"); dir != "" {
203
+ candidates = append(candidates, filepath.Join(dir, engineBinaryName()))
204
+ }
205
+ home, _ := os.UserHomeDir()
206
+ if home != "" {
207
+ candidates = append(candidates, filepath.Join(home, ".local", "bin", engineBinaryName()))
208
+ }
209
+ return append(candidates, filepath.Join("/usr/local/bin", engineBinaryName()))
210
+ }
211
+
212
+ func engineVersion(path string) string {
213
+ got, err := readEngineVersion(path, 30*time.Second)
214
+ if err != nil {
215
+ return ""
216
+ }
217
+ return got
218
+ }
219
+
220
+ func verifyEngineBinary(path, want string, timeout time.Duration) (string, error) {
221
+ want = strings.TrimPrefix(strings.TrimSpace(want), "v")
222
+ if want == "" || want == "dev" || !semverLike(want) {
223
+ return "", fmt.Errorf("invalid requested version %q", want)
224
+ }
225
+ got, err := readEngineVersion(path, timeout)
226
+ if err != nil {
227
+ return "", err
228
+ }
229
+ if strings.TrimPrefix(got, "v") != want {
230
+ return "", fmt.Errorf("version mismatch: got %s want %s", got, want)
231
+ }
232
+ return got, nil
233
+ }
234
+
235
+ func readEngineVersion(path string, timeout time.Duration) (string, error) {
236
+ ctx, cancel := context.WithTimeout(context.Background(), timeout)
237
+ defer cancel()
238
+ out, err := exec.CommandContext(ctx, path, "version").Output()
239
+ if err != nil {
240
+ if errors.Is(ctx.Err(), context.DeadlineExceeded) {
241
+ return "", fmt.Errorf("version command timed out after %s", timeout)
242
+ }
243
+ return "", fmt.Errorf("run exact path %s version: %w", path, err)
244
+ }
245
+ line := strings.TrimSuffix(string(out), "\n")
246
+ line = strings.TrimSuffix(line, "\r")
247
+ if strings.ContainsAny(line, "\r\n") {
248
+ return "", fmt.Errorf("invalid multi-line version output")
249
+ }
250
+ line = strings.TrimSpace(line)
251
+ if line == "" || line == "dev" || !semverLike(line) {
252
+ return "", fmt.Errorf("invalid version output %q", line)
253
+ }
254
+ return line, nil
255
+ }