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.
- package/CHANGELOG.md +12 -0
- package/NOTICE.md +0 -13
- 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-retrospector.md +2 -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/SKILL.md +4 -7
- package/pack/.claude/skills/devrites-frontend-craft/reference/quality-standards.md +9 -4
- 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 +9 -7
- package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +1 -1
- 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/core.md +3 -6
- package/pack/.claude/skills/devrites-lib/reference/standards/data-integrity.md +4 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/development-workflow.md +8 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/documentation.md +6 -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 +14 -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/performance.md +4 -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 +10 -2
- package/pack/.claude/skills/devrites-lib/reference/standards/skill-authoring.md +8 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/spec-grammar.md +5 -6
- package/pack/.claude/skills/devrites-lib/reference/standards/testing.md +2 -2
- package/pack/.claude/skills/devrites-lib/reference/standards/tooling.md +1 -1
- package/pack/.claude/skills/rite-define/reference/plan-template.md +5 -2
- package/pack/.claude/skills/rite-learn/SKILL.md +6 -0
- package/pack/.claude/skills/rite-polish/SKILL.md +4 -0
- package/pack/.claude/skills/rite-polish/reference/anti-ai-slop.md +12 -53
- package/pack/.claude/skills/rite-pr-feedback/SKILL.md +7 -2
- package/pack/.claude/skills/rite-spec/reference/spec-template.md +5 -4
- package/pack/.claude/skills/rite-status/SKILL.md +2 -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-retrospector.md +2 -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/SKILL.md +4 -7
- package/pack/generated/claude/skills/devrites-frontend-craft/reference/quality-standards.md +9 -4
- 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 +9 -7
- package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +1 -1
- 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/core.md +3 -6
- package/pack/generated/claude/skills/devrites-lib/reference/standards/data-integrity.md +4 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/development-workflow.md +8 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/documentation.md +6 -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 +14 -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/performance.md +4 -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 +10 -2
- package/pack/generated/claude/skills/devrites-lib/reference/standards/skill-authoring.md +8 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/spec-grammar.md +5 -6
- package/pack/generated/claude/skills/devrites-lib/reference/standards/testing.md +2 -2
- package/pack/generated/claude/skills/devrites-lib/reference/standards/tooling.md +1 -1
- package/pack/generated/claude/skills/rite-define/reference/plan-template.md +5 -2
- package/pack/generated/claude/skills/rite-learn/SKILL.md +6 -0
- package/pack/generated/claude/skills/rite-polish/SKILL.md +4 -0
- package/pack/generated/claude/skills/rite-polish/reference/anti-ai-slop.md +12 -53
- package/pack/generated/claude/skills/rite-pr-feedback/SKILL.md +7 -2
- package/pack/generated/claude/skills/rite-spec/reference/spec-template.md +5 -4
- package/pack/generated/claude/skills/rite-status/SKILL.md +2 -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-retrospector.toml +2 -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/SKILL.md +4 -7
- package/pack/generated/codex/skills/devrites-frontend-craft/reference/quality-standards.md +9 -4
- 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 +9 -7
- package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +1 -1
- 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/core.md +3 -6
- package/pack/generated/codex/skills/devrites-lib/reference/standards/data-integrity.md +4 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/development-workflow.md +8 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/documentation.md +6 -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 +14 -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/performance.md +4 -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 +10 -2
- package/pack/generated/codex/skills/devrites-lib/reference/standards/skill-authoring.md +8 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/spec-grammar.md +5 -6
- package/pack/generated/codex/skills/devrites-lib/reference/standards/testing.md +2 -2
- package/pack/generated/codex/skills/devrites-lib/reference/standards/tooling.md +1 -1
- package/pack/generated/codex/skills/rite-define/reference/plan-template.md +5 -2
- package/pack/generated/codex/skills/rite-learn/SKILL.md +6 -0
- package/pack/generated/codex/skills/rite-polish/SKILL.md +4 -0
- package/pack/generated/codex/skills/rite-polish/reference/anti-ai-slop.md +12 -53
- package/pack/generated/codex/skills/rite-pr-feedback/SKILL.md +7 -2
- package/pack/generated/codex/skills/rite-spec/reference/spec-template.md +5 -4
- package/pack/generated/codex/skills/rite-status/SKILL.md +2 -0
- package/package.json +1 -1
- package/docs/markdown-instruction-upgrade-2026-08-27.md +0 -127
|
@@ -1,31 +1,17 @@
|
|
|
1
1
|
package install
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
-
"bytes"
|
|
5
|
-
"context"
|
|
6
|
-
"crypto/sha256"
|
|
7
|
-
"encoding/hex"
|
|
8
|
-
"encoding/json"
|
|
9
4
|
"errors"
|
|
10
5
|
"flag"
|
|
11
6
|
"fmt"
|
|
12
7
|
"io"
|
|
13
8
|
"io/fs"
|
|
14
9
|
"os"
|
|
15
|
-
"os/exec"
|
|
16
10
|
"path/filepath"
|
|
17
|
-
"runtime"
|
|
18
|
-
"slices"
|
|
19
|
-
"sort"
|
|
20
11
|
"strings"
|
|
21
|
-
"time"
|
|
22
12
|
|
|
23
13
|
"github.com/devrites/devrites/internal/devritespaths"
|
|
24
|
-
"github.com/devrites/devrites/internal/fsutil"
|
|
25
14
|
"github.com/devrites/devrites/internal/hostpack"
|
|
26
|
-
"github.com/devrites/devrites/internal/release"
|
|
27
|
-
"github.com/devrites/devrites/internal/safepath"
|
|
28
|
-
"github.com/devrites/devrites/internal/version"
|
|
29
15
|
)
|
|
30
16
|
|
|
31
17
|
const ManifestName = devritespaths.ManifestName
|
|
@@ -349,1588 +335,6 @@ func (r *runner) validatePayload() error {
|
|
|
349
335
|
return nil
|
|
350
336
|
}
|
|
351
337
|
|
|
352
|
-
func (r *runner) install() error {
|
|
353
|
-
fmt.Fprintln(r.opts.Stdout, "DevRites installer")
|
|
354
|
-
fmt.Fprintf(r.opts.Stdout, " target : %s\n", r.target)
|
|
355
|
-
fmt.Fprintf(r.opts.Stdout, " payload: %s\n", r.payload)
|
|
356
|
-
fmt.Fprintf(r.opts.Stdout, " skills : %s\n", yesno(r.opts.WithSkills))
|
|
357
|
-
fmt.Fprintln(r.opts.Stdout, " standards: ship inside the devrites-lib skill")
|
|
358
|
-
fmt.Fprintf(r.opts.Stdout, " agents : %s\n", yesno(r.opts.WithAgents))
|
|
359
|
-
fmt.Fprintf(r.opts.Stdout, " codex : %s\n", yesno(r.opts.WithCodex))
|
|
360
|
-
fmt.Fprintf(r.opts.Stdout, " aliases: %s\n", r.opts.AliasMode)
|
|
361
|
-
if r.opts.DryRun {
|
|
362
|
-
fmt.Fprintln(r.opts.Stdout, " (dry run - no changes will be made)")
|
|
363
|
-
}
|
|
364
|
-
fmt.Fprintln(r.opts.Stdout)
|
|
365
|
-
if r.preparedBinary != "" {
|
|
366
|
-
if _, err := verifyEngineBinary(r.preparedBinary, r.binaryTag(), 30*time.Second); err != nil {
|
|
367
|
-
return fmt.Errorf("verify staged engine binary %s: %w", r.preparedBinary, err)
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
if err := r.preflightInstall(); err != nil {
|
|
371
|
-
return err
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
for _, tree := range hostpack.InstallTrees(r.opts.WithSkills, r.opts.WithAgents, r.opts.WithCodex) {
|
|
375
|
-
if err := r.installTree(tree.PayloadPrefix, tree.TargetPrefix); err != nil {
|
|
376
|
-
return fmt.Errorf("install tree %s: %w", tree.TargetPrefix, err)
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
if r.opts.WithSkills && r.opts.AliasMode == "all" {
|
|
380
|
-
for _, alias := range hostpack.Aliases {
|
|
381
|
-
data, err := hostpack.RenderAliasSkill(alias)
|
|
382
|
-
if err != nil {
|
|
383
|
-
return fmt.Errorf("render alias skill %s: %w", alias.Name, err)
|
|
384
|
-
}
|
|
385
|
-
for _, rel := range hostpack.AliasTargets(alias, r.opts.WithCodex) {
|
|
386
|
-
if err := r.installData(data, rel); err != nil {
|
|
387
|
-
return fmt.Errorf("install alias: %w", err)
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
if r.opts.WithSkills && r.opts.WithCodex {
|
|
393
|
-
if err := r.mergeMarkerFile(hostpack.CodexAgentsMerge); err != nil {
|
|
394
|
-
return fmt.Errorf("merge %s: %w", hostpack.CodexAgentsMerge.TargetRel, err)
|
|
395
|
-
}
|
|
396
|
-
if err := r.mergeCodexConfig(); err != nil {
|
|
397
|
-
return fmt.Errorf("merge %s: %w", hostpack.CodexConfigMerge.TargetRel, err)
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
if r.opts.WithSkills {
|
|
401
|
-
if err := r.seedClaudeSettings(); err != nil {
|
|
402
|
-
return fmt.Errorf("seed claude settings: %w", err)
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
if err := r.seedDevrites(); err != nil {
|
|
406
|
-
return fmt.Errorf("seed .devrites: %w", err)
|
|
407
|
-
}
|
|
408
|
-
if err := r.pruneDropped(); err != nil {
|
|
409
|
-
return fmt.Errorf("prune dropped files: %w", err)
|
|
410
|
-
}
|
|
411
|
-
if !r.opts.DryRun {
|
|
412
|
-
if err := r.writeManifest(); err != nil {
|
|
413
|
-
return fmt.Errorf("write manifest: %w", err)
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
if err := r.installBinary(); err != nil {
|
|
417
|
-
return fmt.Errorf("install engine binary: %w", err)
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
fmt.Fprintln(r.opts.Stdout)
|
|
421
|
-
if r.opts.DryRun {
|
|
422
|
-
fmt.Fprintln(r.opts.Stdout, "DevRites install plan complete (dry run)")
|
|
423
|
-
} else {
|
|
424
|
-
fmt.Fprintln(r.opts.Stdout, "DevRites installed")
|
|
425
|
-
}
|
|
426
|
-
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)
|
|
427
|
-
if !r.opts.DryRun && r.opts.WithSkills {
|
|
428
|
-
if r.opts.WithCodex {
|
|
429
|
-
fmt.Fprintln(r.opts.Stdout, "Next: reopen the project, then run /rite (Claude) or $rite (Codex).")
|
|
430
|
-
} else {
|
|
431
|
-
fmt.Fprintln(r.opts.Stdout, "Next: reopen the project, then run /rite.")
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
return nil
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
func (r *runner) installTree(srcPrefix, relPrefix string) error {
|
|
438
|
-
return fs.WalkDir(r.payloadFS, srcPrefix, func(path string, d fs.DirEntry, err error) error {
|
|
439
|
-
if err != nil {
|
|
440
|
-
return fmt.Errorf("walk payload %s: %w", path, err)
|
|
441
|
-
}
|
|
442
|
-
if d.IsDir() {
|
|
443
|
-
return nil
|
|
444
|
-
}
|
|
445
|
-
rel, err := filepath.Rel(srcPrefix, path)
|
|
446
|
-
if err != nil {
|
|
447
|
-
return fmt.Errorf("resolve relative path for %s: %w", path, err)
|
|
448
|
-
}
|
|
449
|
-
return r.installFile(path, filepath.ToSlash(filepath.Join(relPrefix, rel)))
|
|
450
|
-
})
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
func (r *runner) installFile(src, rel string) error {
|
|
454
|
-
data, err := fs.ReadFile(r.payloadFS, src)
|
|
455
|
-
if err != nil {
|
|
456
|
-
return fmt.Errorf("read payload %s: %w", src, err)
|
|
457
|
-
}
|
|
458
|
-
return r.installData(data, rel)
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
func (r *runner) preflightInstall() error {
|
|
462
|
-
desired, err := r.desiredInstallPaths()
|
|
463
|
-
if err != nil {
|
|
464
|
-
return fmt.Errorf("preflight install paths: %w", err)
|
|
465
|
-
}
|
|
466
|
-
var conflicts []string
|
|
467
|
-
for rel := range desired {
|
|
468
|
-
snapshot, err := r.rememberPath(rel)
|
|
469
|
-
if err != nil {
|
|
470
|
-
return err
|
|
471
|
-
}
|
|
472
|
-
record, managed := r.prev[rel]
|
|
473
|
-
if managed && !snapshot.missing {
|
|
474
|
-
if kind := managedConflict(record, snapshot); kind != "" && !r.opts.Force {
|
|
475
|
-
conflicts = append(conflicts, fmt.Sprintf("%s: %s", kind, rel))
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
for rel, record := range r.prev {
|
|
480
|
-
if desired[rel] || hostpack.PreserveOnPrune(rel) {
|
|
481
|
-
continue
|
|
482
|
-
}
|
|
483
|
-
snapshot, err := r.rememberPath(rel)
|
|
484
|
-
if err != nil {
|
|
485
|
-
return err
|
|
486
|
-
}
|
|
487
|
-
if !snapshot.missing {
|
|
488
|
-
if kind := managedConflict(record, snapshot); kind != "" && !r.opts.Force {
|
|
489
|
-
conflicts = append(conflicts, fmt.Sprintf("%s: %s", kind, rel))
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
if merge, ok := hostpack.ManagedMergeForMarker(rel); ok {
|
|
493
|
-
if _, err := r.rememberPath(merge.TargetRel); err != nil {
|
|
494
|
-
return err
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
for _, rel := range r.installMergeTargets() {
|
|
499
|
-
if _, err := r.rememberPath(rel); err != nil {
|
|
500
|
-
return err
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
if _, err := r.rememberPath(ManifestName); err != nil {
|
|
504
|
-
return err
|
|
505
|
-
}
|
|
506
|
-
return managedConflictError(conflicts)
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
func (r *runner) preflightUninstall(entries []string) error {
|
|
510
|
-
var conflicts []string
|
|
511
|
-
for _, rel := range entries {
|
|
512
|
-
if merge, ok := hostpack.ManagedMergeForMarker(rel); ok {
|
|
513
|
-
if _, err := r.rememberPath(merge.TargetRel); err != nil {
|
|
514
|
-
return err
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
if !hostpack.ShouldRemoveOnUninstall(rel, entries) {
|
|
518
|
-
continue
|
|
519
|
-
}
|
|
520
|
-
snapshot, err := r.rememberPath(rel)
|
|
521
|
-
if err != nil {
|
|
522
|
-
return err
|
|
523
|
-
}
|
|
524
|
-
if snapshot.missing {
|
|
525
|
-
continue
|
|
526
|
-
}
|
|
527
|
-
if kind := managedConflict(r.prev[rel], snapshot); kind != "" && !r.opts.Force {
|
|
528
|
-
conflicts = append(conflicts, fmt.Sprintf("%s: %s", kind, rel))
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
if _, err := r.rememberPath(ManifestName); err != nil {
|
|
532
|
-
return err
|
|
533
|
-
}
|
|
534
|
-
return managedConflictError(conflicts)
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
func (r *runner) desiredInstallPaths() (map[string]bool, error) {
|
|
538
|
-
out := map[string]bool{".devrites/README.md": true}
|
|
539
|
-
for _, tree := range hostpack.InstallTrees(r.opts.WithSkills, r.opts.WithAgents, r.opts.WithCodex) {
|
|
540
|
-
err := fs.WalkDir(r.payloadFS, tree.PayloadPrefix, func(path string, d fs.DirEntry, err error) error {
|
|
541
|
-
if err != nil {
|
|
542
|
-
return err
|
|
543
|
-
}
|
|
544
|
-
if d.IsDir() {
|
|
545
|
-
return nil
|
|
546
|
-
}
|
|
547
|
-
rel, err := filepath.Rel(tree.PayloadPrefix, path)
|
|
548
|
-
if err != nil {
|
|
549
|
-
return err
|
|
550
|
-
}
|
|
551
|
-
out[filepath.ToSlash(filepath.Join(tree.TargetPrefix, rel))] = true
|
|
552
|
-
return nil
|
|
553
|
-
})
|
|
554
|
-
if err != nil {
|
|
555
|
-
return nil, err
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
if r.opts.WithSkills && r.opts.AliasMode == "all" {
|
|
559
|
-
for _, alias := range hostpack.Aliases {
|
|
560
|
-
for _, rel := range hostpack.AliasTargets(alias, r.opts.WithCodex) {
|
|
561
|
-
out[rel] = true
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
if r.opts.WithSkills && r.opts.WithCodex {
|
|
566
|
-
out[hostpack.CodexAgentsMerge.MarkerRel] = true
|
|
567
|
-
out[hostpack.CodexConfigMerge.MarkerRel] = true
|
|
568
|
-
}
|
|
569
|
-
if r.opts.WithSkills {
|
|
570
|
-
out[hostpack.ClaudeSettingsMerge.MarkerRel] = true
|
|
571
|
-
}
|
|
572
|
-
return out, nil
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
func (r *runner) installMergeTargets() []string {
|
|
576
|
-
var out []string
|
|
577
|
-
if r.opts.WithSkills {
|
|
578
|
-
out = append(out, hostpack.ClaudeSettingsMerge.TargetRel)
|
|
579
|
-
if r.opts.WithCodex {
|
|
580
|
-
out = append(out, hostpack.CodexAgentsMerge.TargetRel, hostpack.CodexConfigMerge.TargetRel)
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
return out
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
func (r *runner) rememberPath(rel string) (pathSnapshot, error) {
|
|
587
|
-
rel = filepath.ToSlash(rel)
|
|
588
|
-
if snapshot, ok := r.preflight[rel]; ok {
|
|
589
|
-
return snapshot, nil
|
|
590
|
-
}
|
|
591
|
-
snapshot, err := inspectManagedPath(r.target, rel)
|
|
592
|
-
if err != nil {
|
|
593
|
-
return pathSnapshot{}, err
|
|
594
|
-
}
|
|
595
|
-
r.preflight[rel] = snapshot
|
|
596
|
-
return snapshot, nil
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
func (r *runner) recheckPath(rel string) error {
|
|
600
|
-
rel = filepath.ToSlash(rel)
|
|
601
|
-
before, ok := r.preflight[rel]
|
|
602
|
-
if !ok {
|
|
603
|
-
return fmt.Errorf("path %s was not preflighted", rel)
|
|
604
|
-
}
|
|
605
|
-
now, err := inspectManagedPath(r.target, rel)
|
|
606
|
-
if err != nil {
|
|
607
|
-
return err
|
|
608
|
-
}
|
|
609
|
-
if now != before {
|
|
610
|
-
return fmt.Errorf("refusing to change %s: file changed after preflight; retry the operation", rel)
|
|
611
|
-
}
|
|
612
|
-
return nil
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
func inspectManagedPath(target, rel string) (pathSnapshot, error) {
|
|
616
|
-
native := filepath.FromSlash(rel)
|
|
617
|
-
if !filepath.IsLocal(native) {
|
|
618
|
-
return pathSnapshot{}, fmt.Errorf("refusing unsafe managed path %q", rel)
|
|
619
|
-
}
|
|
620
|
-
dest := filepath.Join(target, native)
|
|
621
|
-
if !safepath.WithinResolved(dest, target) {
|
|
622
|
-
return pathSnapshot{}, fmt.Errorf("refusing managed path outside target: %s", rel)
|
|
623
|
-
}
|
|
624
|
-
walk := target
|
|
625
|
-
parts := strings.Split(native, string(filepath.Separator))
|
|
626
|
-
for _, part := range parts {
|
|
627
|
-
walk = filepath.Join(walk, part)
|
|
628
|
-
info, err := os.Lstat(walk)
|
|
629
|
-
if os.IsNotExist(err) {
|
|
630
|
-
return pathSnapshot{missing: true}, nil
|
|
631
|
-
}
|
|
632
|
-
if err != nil {
|
|
633
|
-
return pathSnapshot{}, fmt.Errorf("inspect managed path %s: %w", rel, err)
|
|
634
|
-
}
|
|
635
|
-
if info.Mode()&os.ModeSymlink != 0 {
|
|
636
|
-
return pathSnapshot{}, fmt.Errorf("refusing symlink or junction in managed path: %s", rel)
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
info, err := os.Lstat(dest)
|
|
640
|
-
if os.IsNotExist(err) {
|
|
641
|
-
return pathSnapshot{missing: true}, nil
|
|
642
|
-
}
|
|
643
|
-
if err != nil {
|
|
644
|
-
return pathSnapshot{}, fmt.Errorf("inspect managed path %s: %w", rel, err)
|
|
645
|
-
}
|
|
646
|
-
if !info.Mode().IsRegular() {
|
|
647
|
-
return pathSnapshot{}, fmt.Errorf("refusing non-regular managed path: %s", rel)
|
|
648
|
-
}
|
|
649
|
-
f, err := os.Open(dest)
|
|
650
|
-
if err != nil {
|
|
651
|
-
return pathSnapshot{}, fmt.Errorf("read managed path %s: %w", rel, err)
|
|
652
|
-
}
|
|
653
|
-
defer f.Close()
|
|
654
|
-
sum := sha256.New()
|
|
655
|
-
if _, err := io.Copy(sum, f); err != nil {
|
|
656
|
-
return pathSnapshot{}, fmt.Errorf("hash managed path %s: %w", rel, err)
|
|
657
|
-
}
|
|
658
|
-
return pathSnapshot{hash: "sha256:" + hex.EncodeToString(sum.Sum(nil))}, nil
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
func managedConflict(record managedRecord, snapshot pathSnapshot) string {
|
|
662
|
-
if record.Hash == "" {
|
|
663
|
-
return "legacy manifest entry (no hash)"
|
|
664
|
-
}
|
|
665
|
-
if record.Hash != snapshot.hash {
|
|
666
|
-
return "customized managed file"
|
|
667
|
-
}
|
|
668
|
-
return ""
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
func managedConflictError(conflicts []string) error {
|
|
672
|
-
if len(conflicts) == 0 {
|
|
673
|
-
return nil
|
|
674
|
-
}
|
|
675
|
-
sort.Strings(conflicts)
|
|
676
|
-
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 "))
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
func (r *runner) installData(data []byte, rel string) error {
|
|
680
|
-
dest := filepath.Join(r.target, filepath.FromSlash(rel))
|
|
681
|
-
action := "install"
|
|
682
|
-
if exists(dest) {
|
|
683
|
-
record, managed := r.prev[rel]
|
|
684
|
-
switch {
|
|
685
|
-
case managed && managedConflict(record, r.preflight[filepath.ToSlash(rel)]) != "" && r.opts.Force:
|
|
686
|
-
action = "overwrite(force-customized)"
|
|
687
|
-
case managed:
|
|
688
|
-
action = "overwrite"
|
|
689
|
-
case r.opts.Force:
|
|
690
|
-
action = "overwrite(force)"
|
|
691
|
-
default:
|
|
692
|
-
if r.opts.DryRun {
|
|
693
|
-
fmt.Fprintf(r.opts.Stdout, " [skip] %s (exists, not DevRites-managed)\n", rel)
|
|
694
|
-
} else {
|
|
695
|
-
fmt.Fprintf(r.opts.Stderr, "warning: skip %s (exists, not DevRites-managed; use --force to overwrite)\n", rel)
|
|
696
|
-
}
|
|
697
|
-
r.stats.skipped++
|
|
698
|
-
return nil
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
if r.opts.DryRun {
|
|
702
|
-
fmt.Fprintf(r.opts.Stdout, " [%s] %s\n", action, rel)
|
|
703
|
-
} else {
|
|
704
|
-
if err := r.recheckPath(rel); err != nil {
|
|
705
|
-
return err
|
|
706
|
-
}
|
|
707
|
-
if err := fsutil.WriteFileAtomic(dest, data, 0o644); err != nil {
|
|
708
|
-
return fmt.Errorf("cannot write %s: %w", rel, err)
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
r.addManifest(rel)
|
|
712
|
-
r.addInstallRecord(rel, data)
|
|
713
|
-
if action == "install" {
|
|
714
|
-
r.stats.installed++
|
|
715
|
-
} else {
|
|
716
|
-
r.stats.overwrote++
|
|
717
|
-
}
|
|
718
|
-
return nil
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
func (r *runner) addManifest(rel string) {
|
|
722
|
-
r.manifest = append(r.manifest, filepath.ToSlash(rel))
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
func (r *runner) addInstallRecord(rel string, data []byte) {
|
|
726
|
-
if r.records == nil {
|
|
727
|
-
r.records = map[string]string{}
|
|
728
|
-
}
|
|
729
|
-
sum := sha256.Sum256(data)
|
|
730
|
-
r.records[filepath.ToSlash(rel)] = fmt.Sprintf("sha256:%x", sum[:])
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
func (r *runner) installMarker(rel, text string) error {
|
|
734
|
-
return r.installData([]byte(text+"\n"), rel)
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
func (r *runner) installClaudeSettingsMarker(ownsDefaultMode bool) error {
|
|
738
|
-
ownership := "preexisting"
|
|
739
|
-
if ownsDefaultMode {
|
|
740
|
-
ownership = "added"
|
|
741
|
-
}
|
|
742
|
-
text := hostpack.ClaudeSettingsMerge.MarkerText + "\ndefault-mode=" + ownership
|
|
743
|
-
return r.installMarker(hostpack.ClaudeSettingsMerge.MarkerRel, text)
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
func (r *runner) claudeDefaultModeOwned() bool {
|
|
747
|
-
data, err := os.ReadFile(filepath.Join(r.target, filepath.FromSlash(hostpack.ClaudeSettingsMerge.MarkerRel)))
|
|
748
|
-
return err == nil && strings.Contains(string(data), "\ndefault-mode=added\n")
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
func (r *runner) mergeMarkerFile(merge hostpack.MarkerMerge) error {
|
|
752
|
-
block, err := fs.ReadFile(r.payloadFS, merge.PayloadRel)
|
|
753
|
-
if err != nil {
|
|
754
|
-
return fmt.Errorf("read payload %s: %w", merge.PayloadRel, err)
|
|
755
|
-
}
|
|
756
|
-
dest := filepath.Join(r.target, filepath.FromSlash(merge.TargetRel))
|
|
757
|
-
if r.opts.DryRun {
|
|
758
|
-
verb := "create DevRites block"
|
|
759
|
-
current, readErr := os.ReadFile(dest)
|
|
760
|
-
if readErr == nil {
|
|
761
|
-
if bytes.Contains(current, []byte(merge.Begin)) {
|
|
762
|
-
verb = "refresh DevRites block"
|
|
763
|
-
} else {
|
|
764
|
-
verb = "append DevRites block"
|
|
765
|
-
}
|
|
766
|
-
} else if !errors.Is(readErr, fs.ErrNotExist) {
|
|
767
|
-
return fmt.Errorf("cannot read %s: %w", merge.TargetRel, readErr)
|
|
768
|
-
}
|
|
769
|
-
fmt.Fprintf(r.opts.Stdout, " [merge] %s (%s)\n", merge.TargetRel, verb)
|
|
770
|
-
} else {
|
|
771
|
-
if err := r.recheckPath(merge.TargetRel); err != nil {
|
|
772
|
-
return err
|
|
773
|
-
}
|
|
774
|
-
next := block
|
|
775
|
-
current, readErr := os.ReadFile(dest)
|
|
776
|
-
if readErr == nil {
|
|
777
|
-
next = hostpack.MergeMarkerBlock(current, block, merge.Begin, merge.End)
|
|
778
|
-
} else if !errors.Is(readErr, fs.ErrNotExist) {
|
|
779
|
-
return fmt.Errorf("cannot read %s: %w", merge.TargetRel, readErr)
|
|
780
|
-
}
|
|
781
|
-
if err := fsutil.WriteFileAtomic(dest, next, 0o644); err != nil {
|
|
782
|
-
return fmt.Errorf("cannot write %s: %w", merge.TargetRel, err)
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
return r.installMarker(merge.MarkerRel, merge.MarkerText)
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
func (r *runner) mergeCodexConfig() error {
|
|
789
|
-
merge := hostpack.CodexConfigMerge
|
|
790
|
-
block, err := fs.ReadFile(r.payloadFS, merge.PayloadRel)
|
|
791
|
-
if err != nil {
|
|
792
|
-
return fmt.Errorf("read payload %s: %w", merge.PayloadRel, err)
|
|
793
|
-
}
|
|
794
|
-
dest := filepath.Join(r.target, filepath.FromSlash(merge.TargetRel))
|
|
795
|
-
if r.opts.DryRun {
|
|
796
|
-
fmt.Fprintf(r.opts.Stdout, " [merge] %s (prepend DevRites permission block)\n", merge.TargetRel)
|
|
797
|
-
return r.installMarker(merge.MarkerRel, merge.MarkerText)
|
|
798
|
-
}
|
|
799
|
-
if err := r.recheckPath(merge.TargetRel); err != nil {
|
|
800
|
-
return err
|
|
801
|
-
}
|
|
802
|
-
var current []byte
|
|
803
|
-
if data, readErr := os.ReadFile(dest); readErr == nil {
|
|
804
|
-
current = stripMarkerBlock(data, merge.Begin, merge.End)
|
|
805
|
-
current = stripMarkerBlock(current, "# BEGIN DEVRITES CODEX MCP", "# END DEVRITES CODEX MCP")
|
|
806
|
-
current = stripMarkerBlock(current, "### BEGIN DEVRITES CODEX MCP", "### END DEVRITES CODEX MCP")
|
|
807
|
-
if hasTopLevelTOMLKey(current, "default_permissions") {
|
|
808
|
-
return fmt.Errorf("%s already sets top-level default_permissions; remove that project override before installing the DevRites read-only-root profile", merge.TargetRel)
|
|
809
|
-
}
|
|
810
|
-
} else if !errors.Is(readErr, fs.ErrNotExist) {
|
|
811
|
-
return fmt.Errorf("cannot read %s: %w", merge.TargetRel, readErr)
|
|
812
|
-
}
|
|
813
|
-
next := append([]byte(nil), block...)
|
|
814
|
-
if len(next) == 0 || next[len(next)-1] != '\n' {
|
|
815
|
-
next = append(next, '\n')
|
|
816
|
-
}
|
|
817
|
-
if len(bytes.TrimSpace(current)) > 0 {
|
|
818
|
-
next = append(next, '\n')
|
|
819
|
-
next = append(next, current...)
|
|
820
|
-
}
|
|
821
|
-
if err := fsutil.WriteFileAtomic(dest, next, 0o644); err != nil {
|
|
822
|
-
return fmt.Errorf("cannot write %s: %w", merge.TargetRel, err)
|
|
823
|
-
}
|
|
824
|
-
return r.installMarker(merge.MarkerRel, merge.MarkerText)
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
func (r *runner) mergeClaudeSettings(merge hostpack.JSONMerge) error {
|
|
828
|
-
dest := filepath.Join(r.target, filepath.FromSlash(merge.TargetRel))
|
|
829
|
-
devrites, err := readJSONFS(r.payloadFS, merge.PayloadRel)
|
|
830
|
-
if err != nil {
|
|
831
|
-
return fmt.Errorf("load Claude settings payload: %w", err)
|
|
832
|
-
}
|
|
833
|
-
current := map[string]any{}
|
|
834
|
-
if data, readErr := readJSON(dest); readErr == nil {
|
|
835
|
-
current = data
|
|
836
|
-
} else if !errors.Is(readErr, fs.ErrNotExist) {
|
|
837
|
-
return fmt.Errorf("load existing Claude settings: %w", readErr)
|
|
838
|
-
}
|
|
839
|
-
next, ownsDefaultMode, err := mergeClaudeSettingsConfig(current, devrites, r.claudeDefaultModeOwned())
|
|
840
|
-
if err != nil {
|
|
841
|
-
return err
|
|
842
|
-
}
|
|
843
|
-
if r.opts.DryRun {
|
|
844
|
-
fmt.Fprintf(r.opts.Stdout, " [merge] %s\n", merge.DryRunText)
|
|
845
|
-
return r.installClaudeSettingsMarker(ownsDefaultMode)
|
|
846
|
-
}
|
|
847
|
-
if err := r.recheckPath(merge.TargetRel); err != nil {
|
|
848
|
-
return err
|
|
849
|
-
}
|
|
850
|
-
data, err := json.MarshalIndent(next, "", " ")
|
|
851
|
-
if err != nil {
|
|
852
|
-
return fmt.Errorf("encode merged Claude settings: %w", err)
|
|
853
|
-
}
|
|
854
|
-
data = append(data, '\n')
|
|
855
|
-
if err := fsutil.WriteFileAtomic(dest, data, 0o644); err != nil {
|
|
856
|
-
return fmt.Errorf("cannot write %s: %w", merge.TargetRel, err)
|
|
857
|
-
}
|
|
858
|
-
return r.installClaudeSettingsMarker(ownsDefaultMode)
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
func (r *runner) seedClaudeSettings() error {
|
|
862
|
-
return r.mergeClaudeSettings(hostpack.ClaudeSettingsMerge)
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
func (r *runner) seedDevrites() error {
|
|
866
|
-
readme, err := hostpack.RenderDevritesReadme()
|
|
867
|
-
if err != nil {
|
|
868
|
-
return fmt.Errorf("render readme: %w", err)
|
|
869
|
-
}
|
|
870
|
-
if err := r.installData(readme, ".devrites/README.md"); err != nil {
|
|
871
|
-
return fmt.Errorf("install readme: %w", err)
|
|
872
|
-
}
|
|
873
|
-
active := filepath.Join(r.target, ".devrites", "ACTIVE")
|
|
874
|
-
if exists(active) {
|
|
875
|
-
return nil
|
|
876
|
-
}
|
|
877
|
-
if r.opts.DryRun {
|
|
878
|
-
fmt.Fprintln(r.opts.Stdout, " [seed] .devrites/ACTIVE (runtime state - preserved on uninstall)")
|
|
879
|
-
return nil
|
|
880
|
-
}
|
|
881
|
-
if err := os.MkdirAll(filepath.Dir(active), 0o755); err != nil {
|
|
882
|
-
return fmt.Errorf("create %s: %w", filepath.Dir(active), err)
|
|
883
|
-
}
|
|
884
|
-
f, err := os.OpenFile(active, os.O_CREATE|os.O_EXCL, 0o644)
|
|
885
|
-
if err != nil {
|
|
886
|
-
if os.IsExist(err) {
|
|
887
|
-
return nil
|
|
888
|
-
}
|
|
889
|
-
return fmt.Errorf("create %s: %w", active, err)
|
|
890
|
-
}
|
|
891
|
-
return f.Close()
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
func (r *runner) pruneDropped() error {
|
|
895
|
-
if len(r.prev) == 0 {
|
|
896
|
-
return nil
|
|
897
|
-
}
|
|
898
|
-
next := map[string]bool{}
|
|
899
|
-
for _, rel := range r.manifest {
|
|
900
|
-
next[rel] = true
|
|
901
|
-
}
|
|
902
|
-
keys := make([]string, 0, len(r.prev))
|
|
903
|
-
for rel := range r.prev {
|
|
904
|
-
keys = append(keys, rel)
|
|
905
|
-
}
|
|
906
|
-
sort.Strings(keys)
|
|
907
|
-
for _, rel := range keys {
|
|
908
|
-
if next[rel] || hostpack.PreserveOnPrune(rel) {
|
|
909
|
-
continue
|
|
910
|
-
}
|
|
911
|
-
dead := filepath.Join(r.target, filepath.FromSlash(rel))
|
|
912
|
-
if !exists(dead) {
|
|
913
|
-
continue
|
|
914
|
-
}
|
|
915
|
-
action := "prune"
|
|
916
|
-
if managedConflict(r.prev[rel], r.preflight[rel]) != "" && r.opts.Force {
|
|
917
|
-
action = "prune(force-customized)"
|
|
918
|
-
}
|
|
919
|
-
if r.opts.DryRun {
|
|
920
|
-
fmt.Fprintf(r.opts.Stdout, " [%s] %s (dropped from pack)\n", action, rel)
|
|
921
|
-
} else {
|
|
922
|
-
if err := r.recheckPath(rel); err != nil {
|
|
923
|
-
return err
|
|
924
|
-
}
|
|
925
|
-
if merge, ok := hostpack.ManagedMergeForMarker(rel); ok {
|
|
926
|
-
if err := r.recheckPath(merge.TargetRel); err != nil {
|
|
927
|
-
return err
|
|
928
|
-
}
|
|
929
|
-
if merge.MarkerRel == hostpack.LegacyCodexHooksMerge.MarkerRel {
|
|
930
|
-
if err := stripHooksPath(filepath.Join(r.target, filepath.FromSlash(merge.TargetRel))); err != nil {
|
|
931
|
-
return fmt.Errorf("strip hooks from %s: %w", merge.TargetRel, err)
|
|
932
|
-
}
|
|
933
|
-
} else if merge.TargetRel == hostpack.ClaudeSettingsMerge.TargetRel {
|
|
934
|
-
_ = r.stripClaudeSettings(filepath.Join(r.target, filepath.FromSlash(merge.TargetRel)), true)
|
|
935
|
-
} else {
|
|
936
|
-
_ = stripMarkerPath(filepath.Join(r.target, filepath.FromSlash(merge.TargetRel)), merge.Begin, merge.End)
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
if err := os.Remove(dead); err != nil && !os.IsNotExist(err) {
|
|
940
|
-
return fmt.Errorf("remove %s: %w", rel, err)
|
|
941
|
-
}
|
|
942
|
-
pruneEmptyDirs(filepath.Dir(dead), r.target)
|
|
943
|
-
fmt.Fprintf(r.opts.Stdout, " [%s] %s\n", action, rel)
|
|
944
|
-
}
|
|
945
|
-
r.stats.pruned++
|
|
946
|
-
}
|
|
947
|
-
return nil
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
func (r *runner) writeManifest() error {
|
|
951
|
-
sort.Strings(r.manifest)
|
|
952
|
-
r.manifest = slices.Compact(r.manifest)
|
|
953
|
-
var b strings.Builder
|
|
954
|
-
b.WriteString("# DevRites install manifest - do not edit by hand.\n")
|
|
955
|
-
b.WriteString("# Generated " + time.Now().UTC().Format(time.RFC3339) + ". Uninstall removes exactly these paths.\n")
|
|
956
|
-
b.WriteString("# devrites-version: " + installedVersion(r.source) + "\n")
|
|
957
|
-
b.WriteString("# devrites-flags: " + r.flagsString() + "\n")
|
|
958
|
-
b.WriteString("# managed-records: source=npx payload=pack/generated format=rel sha256\n")
|
|
959
|
-
for _, rel := range r.manifest {
|
|
960
|
-
if hash := r.records[rel]; hash != "" {
|
|
961
|
-
b.WriteString("# managed: " + rel + " " + hash + "\n")
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
for _, rel := range r.manifest {
|
|
965
|
-
b.WriteString(rel + "\n")
|
|
966
|
-
}
|
|
967
|
-
if err := r.recheckPath(ManifestName); err != nil {
|
|
968
|
-
return err
|
|
969
|
-
}
|
|
970
|
-
return fsutil.WriteFileAtomic(filepath.Join(r.target, ManifestName), []byte(b.String()), 0o644)
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
func (r *runner) flagsString() string {
|
|
974
|
-
var flags []string
|
|
975
|
-
if !r.opts.WithSkills {
|
|
976
|
-
flags = append(flags, "--no-skills")
|
|
977
|
-
}
|
|
978
|
-
if !r.opts.WithAgents {
|
|
979
|
-
flags = append(flags, "--no-agents")
|
|
980
|
-
}
|
|
981
|
-
if !r.opts.WithCodex {
|
|
982
|
-
flags = append(flags, "--no-codex")
|
|
983
|
-
}
|
|
984
|
-
if !r.opts.WithBinary {
|
|
985
|
-
flags = append(flags, "--no-binary")
|
|
986
|
-
}
|
|
987
|
-
if r.opts.AliasMode == "off" {
|
|
988
|
-
flags = append(flags, "--no-short-aliases")
|
|
989
|
-
}
|
|
990
|
-
if r.opts.AliasMode == "all" {
|
|
991
|
-
flags = append(flags, "--short-aliases=all")
|
|
992
|
-
}
|
|
993
|
-
return strings.Join(flags, " ")
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
func (r *runner) uninstall() error {
|
|
997
|
-
mf := filepath.Join(r.target, ManifestName)
|
|
998
|
-
entries, err := readManifestList(mf)
|
|
999
|
-
if err != nil {
|
|
1000
|
-
return err
|
|
1001
|
-
}
|
|
1002
|
-
if len(entries) == 0 {
|
|
1003
|
-
return fmt.Errorf("no DevRites manifest at %s - nothing to uninstall", mf)
|
|
1004
|
-
}
|
|
1005
|
-
fmt.Fprintln(r.opts.Stdout, "DevRites uninstaller")
|
|
1006
|
-
fmt.Fprintf(r.opts.Stdout, " target : %s\n", r.target)
|
|
1007
|
-
fmt.Fprintf(r.opts.Stdout, " manifest: %s\n", mf)
|
|
1008
|
-
if r.opts.DryRun {
|
|
1009
|
-
fmt.Fprintln(r.opts.Stdout, " (dry run - no changes)")
|
|
1010
|
-
}
|
|
1011
|
-
fmt.Fprintln(r.opts.Stdout)
|
|
1012
|
-
if err := r.preflightUninstall(entries); err != nil {
|
|
1013
|
-
return err
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
for _, rel := range entries {
|
|
1017
|
-
merge, ok := hostpack.ManagedMergeForMarker(rel)
|
|
1018
|
-
if !ok {
|
|
1019
|
-
continue
|
|
1020
|
-
}
|
|
1021
|
-
if r.opts.DryRun {
|
|
1022
|
-
fmt.Fprintf(r.opts.Stdout, " [merge-remove] %s\n", merge.DryRun)
|
|
1023
|
-
continue
|
|
1024
|
-
}
|
|
1025
|
-
if err := r.recheckPath(rel); err != nil {
|
|
1026
|
-
return err
|
|
1027
|
-
}
|
|
1028
|
-
if err := r.recheckPath(merge.TargetRel); err != nil {
|
|
1029
|
-
return err
|
|
1030
|
-
}
|
|
1031
|
-
if merge.MarkerRel == hostpack.LegacyCodexHooksMerge.MarkerRel {
|
|
1032
|
-
if err := stripHooksPath(filepath.Join(r.target, filepath.FromSlash(merge.TargetRel))); err != nil {
|
|
1033
|
-
return fmt.Errorf("strip hooks from %s: %w", merge.TargetRel, err)
|
|
1034
|
-
}
|
|
1035
|
-
continue
|
|
1036
|
-
}
|
|
1037
|
-
if merge.TargetRel == hostpack.ClaudeSettingsMerge.TargetRel {
|
|
1038
|
-
if err := r.stripClaudeSettings(filepath.Join(r.target, filepath.FromSlash(merge.TargetRel)), true); err != nil {
|
|
1039
|
-
return fmt.Errorf("strip DevRites settings from %s: %w", merge.TargetRel, err)
|
|
1040
|
-
}
|
|
1041
|
-
continue
|
|
1042
|
-
}
|
|
1043
|
-
if err := stripMarkerPath(filepath.Join(r.target, filepath.FromSlash(merge.TargetRel)), merge.Begin, merge.End); err != nil {
|
|
1044
|
-
return fmt.Errorf("strip marker block from %s: %w", merge.TargetRel, err)
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
var dirs []string
|
|
1049
|
-
for _, rel := range entries {
|
|
1050
|
-
if !hostpack.ShouldRemoveOnUninstall(rel, entries) {
|
|
1051
|
-
continue
|
|
1052
|
-
}
|
|
1053
|
-
dest := filepath.Join(r.target, filepath.FromSlash(rel))
|
|
1054
|
-
if exists(dest) {
|
|
1055
|
-
action := "remove"
|
|
1056
|
-
if managedConflict(r.prev[rel], r.preflight[rel]) != "" && r.opts.Force {
|
|
1057
|
-
action = "remove(force-customized)"
|
|
1058
|
-
}
|
|
1059
|
-
if r.opts.DryRun {
|
|
1060
|
-
fmt.Fprintf(r.opts.Stdout, " [%s] %s\n", action, rel)
|
|
1061
|
-
} else {
|
|
1062
|
-
if err := r.recheckPath(rel); err != nil {
|
|
1063
|
-
return err
|
|
1064
|
-
}
|
|
1065
|
-
if err := os.Remove(dest); err != nil && !os.IsNotExist(err) {
|
|
1066
|
-
return fmt.Errorf("remove %s: %w", rel, err)
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
dirs = append(dirs, filepath.Dir(dest))
|
|
1070
|
-
r.stats.removed++
|
|
1071
|
-
} else {
|
|
1072
|
-
r.stats.missing++
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
if r.opts.DryRun {
|
|
1076
|
-
fmt.Fprintf(r.opts.Stdout, " [remove] %s\n", ManifestName)
|
|
1077
|
-
} else {
|
|
1078
|
-
if err := r.recheckPath(ManifestName); err != nil {
|
|
1079
|
-
return err
|
|
1080
|
-
}
|
|
1081
|
-
_ = os.Remove(mf)
|
|
1082
|
-
dirs = append(dirs, filepath.Dir(mf))
|
|
1083
|
-
for _, d := range dirs {
|
|
1084
|
-
pruneEmptyDirs(d, r.target)
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
if err := r.removeBinary(); err != nil {
|
|
1088
|
-
return fmt.Errorf("remove engine binary: %w", err)
|
|
1089
|
-
}
|
|
1090
|
-
fmt.Fprintln(r.opts.Stdout)
|
|
1091
|
-
if r.opts.DryRun {
|
|
1092
|
-
fmt.Fprintln(r.opts.Stdout, "DevRites uninstall plan complete (dry run)")
|
|
1093
|
-
} else {
|
|
1094
|
-
fmt.Fprintln(r.opts.Stdout, "DevRites uninstalled")
|
|
1095
|
-
}
|
|
1096
|
-
fmt.Fprintf(r.opts.Stdout, " removed: %d already-absent: %d\n", r.stats.removed, r.stats.missing)
|
|
1097
|
-
if exists(filepath.Join(r.target, ".devrites", "work")) {
|
|
1098
|
-
fmt.Fprintln(r.opts.Stdout, " kept .devrites/work/ (your feature data)")
|
|
1099
|
-
}
|
|
1100
|
-
if exists(filepath.Join(r.target, ".devrites", "ACTIVE")) {
|
|
1101
|
-
fmt.Fprintln(r.opts.Stdout, " kept .devrites/ACTIVE (active-feature cursor)")
|
|
1102
|
-
}
|
|
1103
|
-
return nil
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
func stripHooksPath(path string) error {
|
|
1107
|
-
current, err := readJSON(path)
|
|
1108
|
-
if err != nil {
|
|
1109
|
-
if errors.Is(err, fs.ErrNotExist) {
|
|
1110
|
-
return nil
|
|
1111
|
-
}
|
|
1112
|
-
return fmt.Errorf("load hooks config: %w", err)
|
|
1113
|
-
}
|
|
1114
|
-
next := stripDevritesHooks(current)
|
|
1115
|
-
if len(next) == 0 {
|
|
1116
|
-
return os.Remove(path)
|
|
1117
|
-
}
|
|
1118
|
-
data, err := json.MarshalIndent(next, "", " ")
|
|
1119
|
-
if err != nil {
|
|
1120
|
-
return fmt.Errorf("encode hooks config: %w", err)
|
|
1121
|
-
}
|
|
1122
|
-
data = append(data, '\n')
|
|
1123
|
-
return fsutil.WriteFileAtomic(path, data, 0o644)
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
func (r *runner) stripClaudeSettings(path string, preserveEmpty bool) error {
|
|
1127
|
-
current, err := readJSON(path)
|
|
1128
|
-
if err != nil {
|
|
1129
|
-
if errors.Is(err, fs.ErrNotExist) {
|
|
1130
|
-
return nil
|
|
1131
|
-
}
|
|
1132
|
-
return fmt.Errorf("load Claude settings: %w", err)
|
|
1133
|
-
}
|
|
1134
|
-
next := stripDevritesSettings(current, r.claudeDefaultModeOwned())
|
|
1135
|
-
if len(next) == 0 && !preserveEmpty {
|
|
1136
|
-
return os.Remove(path)
|
|
1137
|
-
}
|
|
1138
|
-
data, err := json.MarshalIndent(next, "", " ")
|
|
1139
|
-
if err != nil {
|
|
1140
|
-
return fmt.Errorf("encode Claude settings: %w", err)
|
|
1141
|
-
}
|
|
1142
|
-
data = append(data, '\n')
|
|
1143
|
-
return fsutil.WriteFileAtomic(path, data, 0o644)
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
func runUpdate(opts Options) error {
|
|
1147
|
-
target, err := filepath.Abs(opts.Target)
|
|
1148
|
-
if err != nil {
|
|
1149
|
-
return fmt.Errorf("resolve target %s: %w", opts.Target, err)
|
|
1150
|
-
}
|
|
1151
|
-
mf := filepath.Join(target, ManifestName)
|
|
1152
|
-
if !exists(mf) {
|
|
1153
|
-
return fmt.Errorf("no DevRites install found at %s (missing %s)", target, ManifestName)
|
|
1154
|
-
}
|
|
1155
|
-
installed := manifestHeader(mf, "devrites-version")
|
|
1156
|
-
if installed == "" {
|
|
1157
|
-
installed = "unknown"
|
|
1158
|
-
}
|
|
1159
|
-
if opts.SourceDir == "" {
|
|
1160
|
-
if opts.PayloadDir != "" {
|
|
1161
|
-
return fmt.Errorf("local update with --payload-dir also requires --source-dir")
|
|
1162
|
-
}
|
|
1163
|
-
return runRemoteUpdate(opts, target, installed)
|
|
1164
|
-
}
|
|
1165
|
-
source, err := filepath.Abs(opts.SourceDir)
|
|
1166
|
-
if err != nil {
|
|
1167
|
-
return fmt.Errorf("resolve source %s: %w", opts.SourceDir, err)
|
|
1168
|
-
}
|
|
1169
|
-
candidate := installedVersion(source)
|
|
1170
|
-
if candidate == "" || candidate == "unknown" {
|
|
1171
|
-
return fmt.Errorf("derive update version from local source %s: package.json has no version", source)
|
|
1172
|
-
}
|
|
1173
|
-
candidate = strings.TrimPrefix(candidate, "v")
|
|
1174
|
-
if os.Getenv("DEVRITES_UPDATE_HANDOFF") != "1" {
|
|
1175
|
-
fmt.Fprintln(opts.Stdout, "DevRites update")
|
|
1176
|
-
fmt.Fprintf(opts.Stdout, " project: %s\n", target)
|
|
1177
|
-
fmt.Fprintf(opts.Stdout, " installed: %s\n", installed)
|
|
1178
|
-
fmt.Fprintf(opts.Stdout, " candidate: %s\n", candidate)
|
|
1179
|
-
}
|
|
1180
|
-
if opts.UpdateCheck {
|
|
1181
|
-
if installed == candidate || strings.TrimPrefix(installed, "v") == candidate {
|
|
1182
|
-
fmt.Fprintln(opts.Stdout, "up to date.")
|
|
1183
|
-
return nil
|
|
1184
|
-
}
|
|
1185
|
-
return fmt.Errorf("update available: %s -> %s", installed, candidate)
|
|
1186
|
-
}
|
|
1187
|
-
if !opts.Force && (installed == candidate || strings.TrimPrefix(installed, "v") == candidate) {
|
|
1188
|
-
fmt.Fprintln(opts.Stdout, "already up to date (use --force to reinstall).")
|
|
1189
|
-
return nil
|
|
1190
|
-
}
|
|
1191
|
-
if opts.PayloadDir == "" {
|
|
1192
|
-
return fmt.Errorf("update requires --payload-dir with the locally prepared host payload")
|
|
1193
|
-
}
|
|
1194
|
-
payload, err := filepath.Abs(opts.PayloadDir)
|
|
1195
|
-
if err != nil {
|
|
1196
|
-
return fmt.Errorf("resolve payload %s: %w", opts.PayloadDir, err)
|
|
1197
|
-
}
|
|
1198
|
-
if err := hostpack.ValidatePayload(os.DirFS(payload), true); err != nil {
|
|
1199
|
-
return fmt.Errorf("local update payload under %s is invalid: %w", payload, err)
|
|
1200
|
-
}
|
|
1201
|
-
installFlags := manifestHeader(mf, "devrites-flags")
|
|
1202
|
-
next := DefaultOptions(ModeInstall)
|
|
1203
|
-
next.Stdout = opts.Stdout
|
|
1204
|
-
next.Stderr = opts.Stderr
|
|
1205
|
-
if installFlags != "" {
|
|
1206
|
-
if err := parseArgs(strings.Fields(installFlags), &next); err != nil {
|
|
1207
|
-
return fmt.Errorf("parse manifest install flags: %w", err)
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
next.Target = target
|
|
1211
|
-
next.PayloadDir = payload
|
|
1212
|
-
next.SourceDir = source
|
|
1213
|
-
next.Force = opts.Force
|
|
1214
|
-
if opts.DryRun {
|
|
1215
|
-
next.DryRun = true
|
|
1216
|
-
}
|
|
1217
|
-
r, err := newRunner(next)
|
|
1218
|
-
if err != nil {
|
|
1219
|
-
return fmt.Errorf("prepare installer: %w", err)
|
|
1220
|
-
}
|
|
1221
|
-
r.requiredBinaryTag = "v" + candidate
|
|
1222
|
-
if next.WithBinary && os.Getenv("DEVRITES_NO_BINARY") != "1" && !next.DryRun {
|
|
1223
|
-
staged, cleanup, err := r.acquireBinary(r.requiredBinaryTag)
|
|
1224
|
-
if err != nil {
|
|
1225
|
-
return fmt.Errorf("prepare engine binary: %w", err)
|
|
1226
|
-
}
|
|
1227
|
-
defer cleanup()
|
|
1228
|
-
r.preparedBinary = staged
|
|
1229
|
-
}
|
|
1230
|
-
return r.install()
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
var (
|
|
1234
|
-
resolveLatestRelease = release.Latest
|
|
1235
|
-
acquireRelease = release.Acquire
|
|
1236
|
-
)
|
|
1237
|
-
|
|
1238
|
-
func runRemoteUpdate(opts Options, target, installed string) error {
|
|
1239
|
-
repository := os.Getenv("DEVRITES_REPO")
|
|
1240
|
-
if repository == "" {
|
|
1241
|
-
repository = release.DefaultRepository
|
|
1242
|
-
}
|
|
1243
|
-
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
|
1244
|
-
defer cancel()
|
|
1245
|
-
tag, err := resolveLatestRelease(ctx, repository)
|
|
1246
|
-
if err != nil {
|
|
1247
|
-
return err
|
|
1248
|
-
}
|
|
1249
|
-
latest := strings.TrimPrefix(tag, "v")
|
|
1250
|
-
fmt.Fprintln(opts.Stdout, "DevRites update")
|
|
1251
|
-
fmt.Fprintf(opts.Stdout, " project: %s\n", target)
|
|
1252
|
-
fmt.Fprintf(opts.Stdout, " installed: %s\n", installed)
|
|
1253
|
-
fmt.Fprintf(opts.Stdout, " latest: %s\n", latest)
|
|
1254
|
-
if opts.UpdateCheck {
|
|
1255
|
-
if installed == latest || strings.TrimPrefix(installed, "v") == latest {
|
|
1256
|
-
fmt.Fprintln(opts.Stdout, "up to date.")
|
|
1257
|
-
return nil
|
|
1258
|
-
}
|
|
1259
|
-
return fmt.Errorf("update available: %s -> %s", installed, latest)
|
|
1260
|
-
}
|
|
1261
|
-
if !opts.Force && (installed == latest || strings.TrimPrefix(installed, "v") == latest) {
|
|
1262
|
-
fmt.Fprintln(opts.Stdout, "already up to date (use --force to reinstall).")
|
|
1263
|
-
return nil
|
|
1264
|
-
}
|
|
1265
|
-
candidate, cleanup, err := acquireRelease(ctx, repository, tag)
|
|
1266
|
-
if err != nil {
|
|
1267
|
-
return fmt.Errorf("acquire update %s: %w", tag, err)
|
|
1268
|
-
}
|
|
1269
|
-
defer cleanup()
|
|
1270
|
-
fmt.Fprintf(opts.Stdout, " bundle: %s\n", candidate.BundleURL)
|
|
1271
|
-
if _, err := verifyEngineBinary(candidate.EnginePath, tag, 30*time.Second); err != nil {
|
|
1272
|
-
return fmt.Errorf("verify update engine: %w", err)
|
|
1273
|
-
}
|
|
1274
|
-
args := []string{
|
|
1275
|
-
"update",
|
|
1276
|
-
"--target", target,
|
|
1277
|
-
"--source-dir", candidate.SourceDir,
|
|
1278
|
-
"--payload-dir", candidate.PayloadDir,
|
|
1279
|
-
}
|
|
1280
|
-
if opts.DryRun {
|
|
1281
|
-
args = append(args, "--dry-run")
|
|
1282
|
-
}
|
|
1283
|
-
if opts.Force {
|
|
1284
|
-
args = append(args, "--force")
|
|
1285
|
-
}
|
|
1286
|
-
cmd := exec.CommandContext(ctx, candidate.EnginePath, args...)
|
|
1287
|
-
cmd.Stdout = opts.Stdout
|
|
1288
|
-
cmd.Stderr = opts.Stderr
|
|
1289
|
-
cmd.Env = replaceEnv(os.Environ(), map[string]string{
|
|
1290
|
-
"DEVRITES_ENGINE_CLI": candidate.EnginePath,
|
|
1291
|
-
"DEVRITES_UPDATE_HANDOFF": "1",
|
|
1292
|
-
})
|
|
1293
|
-
if err := cmd.Run(); err != nil {
|
|
1294
|
-
return fmt.Errorf("apply update %s: %w", tag, err)
|
|
1295
|
-
}
|
|
1296
|
-
return nil
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
func replaceEnv(env []string, replacements map[string]string) []string {
|
|
1300
|
-
out := make([]string, 0, len(env)+len(replacements))
|
|
1301
|
-
for _, entry := range env {
|
|
1302
|
-
name, _, found := strings.Cut(entry, "=")
|
|
1303
|
-
if found {
|
|
1304
|
-
if _, replace := replacements[name]; replace {
|
|
1305
|
-
continue
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
out = append(out, entry)
|
|
1309
|
-
}
|
|
1310
|
-
for name, value := range replacements {
|
|
1311
|
-
out = append(out, name+"="+value)
|
|
1312
|
-
}
|
|
1313
|
-
return out
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
func stripMarkerPath(path, begin, end string) error {
|
|
1317
|
-
data, err := os.ReadFile(path)
|
|
1318
|
-
if err != nil {
|
|
1319
|
-
if os.IsNotExist(err) {
|
|
1320
|
-
return nil
|
|
1321
|
-
}
|
|
1322
|
-
return fmt.Errorf("read %s: %w", path, err)
|
|
1323
|
-
}
|
|
1324
|
-
next := stripMarkerBlock(data, begin, end)
|
|
1325
|
-
if strings.TrimSpace(string(next)) == "" {
|
|
1326
|
-
return os.Remove(path)
|
|
1327
|
-
}
|
|
1328
|
-
return fsutil.WriteFileAtomic(path, next, 0o644)
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
func stripMarkerBlock(data []byte, begin, end string) []byte {
|
|
1332
|
-
var out strings.Builder
|
|
1333
|
-
inBlock := false
|
|
1334
|
-
for _, line := range strings.SplitAfter(string(data), "\n") {
|
|
1335
|
-
trim := strings.TrimSuffix(line, "\n")
|
|
1336
|
-
trim = strings.TrimSuffix(trim, "\r")
|
|
1337
|
-
switch trim {
|
|
1338
|
-
case begin:
|
|
1339
|
-
inBlock = true
|
|
1340
|
-
continue
|
|
1341
|
-
case end:
|
|
1342
|
-
inBlock = false
|
|
1343
|
-
continue
|
|
1344
|
-
}
|
|
1345
|
-
if !inBlock {
|
|
1346
|
-
out.WriteString(line)
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
return []byte(out.String())
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
func hasTopLevelTOMLKey(data []byte, key string) bool {
|
|
1353
|
-
for _, line := range strings.Split(string(data), "\n") {
|
|
1354
|
-
line = strings.TrimSpace(line)
|
|
1355
|
-
if line == "" || strings.HasPrefix(line, "#") {
|
|
1356
|
-
continue
|
|
1357
|
-
}
|
|
1358
|
-
if strings.HasPrefix(line, "[") {
|
|
1359
|
-
return false
|
|
1360
|
-
}
|
|
1361
|
-
name, _, ok := strings.Cut(line, "=")
|
|
1362
|
-
if ok && strings.TrimSpace(name) == key {
|
|
1363
|
-
return true
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
return false
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
func readJSON(path string) (map[string]any, error) {
|
|
1370
|
-
data, err := os.ReadFile(path)
|
|
1371
|
-
if err != nil {
|
|
1372
|
-
return nil, fmt.Errorf("read %s: %w", path, err)
|
|
1373
|
-
}
|
|
1374
|
-
return decodeJSON(data)
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
func readJSONFS(src fs.FS, path string) (map[string]any, error) {
|
|
1378
|
-
data, err := fs.ReadFile(src, path)
|
|
1379
|
-
if err != nil {
|
|
1380
|
-
return nil, fmt.Errorf("read %s: %w", path, err)
|
|
1381
|
-
}
|
|
1382
|
-
return decodeJSON(data)
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
func decodeJSON(data []byte) (map[string]any, error) {
|
|
1386
|
-
var out map[string]any
|
|
1387
|
-
if err := json.Unmarshal(data, &out); err != nil {
|
|
1388
|
-
return nil, fmt.Errorf("decode JSON: %w", err)
|
|
1389
|
-
}
|
|
1390
|
-
return out, nil
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
func isDevritesHookCommand(v any) bool {
|
|
1394
|
-
hook, ok := v.(map[string]any)
|
|
1395
|
-
if !ok {
|
|
1396
|
-
return false
|
|
1397
|
-
}
|
|
1398
|
-
command, _ := hook["command"].(string)
|
|
1399
|
-
return strings.Contains(command, "devrites-engine hook ") ||
|
|
1400
|
-
strings.Contains(command, ".claude/hooks/devrites-") ||
|
|
1401
|
-
strings.Contains(command, ".codex/hooks/devrites-") ||
|
|
1402
|
-
(strings.Contains(command, "printf ") && strings.Contains(command, "DevRites:"))
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
func isDevritesHooksComment(comment string) bool {
|
|
1406
|
-
return comment == "DevRites hooks" ||
|
|
1407
|
-
strings.HasPrefix(comment, "DevRites hooks: every event invokes the global `devrites-engine` engine binary") ||
|
|
1408
|
-
strings.HasPrefix(comment, "DevRites hooks: auto-approve the read-only orientation/gate scripts") ||
|
|
1409
|
-
strings.HasPrefix(comment, "DevRites hooks — every event invokes the global `devrites-engine` engine binary") ||
|
|
1410
|
-
strings.HasPrefix(comment, "DevRites hooks — auto-approve the read-only orientation/gate scripts") ||
|
|
1411
|
-
strings.HasPrefix(comment, "DevRites hooks for Codex. Project hooks load only after")
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
func isDevritesSettingsComment(comment string) bool {
|
|
1415
|
-
return isDevritesHooksComment(comment) ||
|
|
1416
|
-
strings.HasPrefix(comment, "DevRites keeps the root orchestration context source-read-only.")
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
func stripDevritesHooks(config map[string]any) map[string]any {
|
|
1420
|
-
next := map[string]any{}
|
|
1421
|
-
for k, v := range config {
|
|
1422
|
-
if k == "$comment" {
|
|
1423
|
-
if s, ok := v.(string); ok && isDevritesHooksComment(s) {
|
|
1424
|
-
continue
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
if k == "statusLine" && isDevritesHookCommand(v) {
|
|
1428
|
-
continue
|
|
1429
|
-
}
|
|
1430
|
-
next[k] = v
|
|
1431
|
-
}
|
|
1432
|
-
rawHooksValue, exists := next["hooks"]
|
|
1433
|
-
if !exists {
|
|
1434
|
-
return next
|
|
1435
|
-
}
|
|
1436
|
-
rawHooks, ok := rawHooksValue.(map[string]any)
|
|
1437
|
-
if !ok {
|
|
1438
|
-
return next
|
|
1439
|
-
}
|
|
1440
|
-
hooks := map[string]any{}
|
|
1441
|
-
for event, entries := range rawHooks {
|
|
1442
|
-
arr, ok := entries.([]any)
|
|
1443
|
-
if !ok {
|
|
1444
|
-
hooks[event] = entries
|
|
1445
|
-
continue
|
|
1446
|
-
}
|
|
1447
|
-
var kept []any
|
|
1448
|
-
for _, entry := range arr {
|
|
1449
|
-
entryMap, ok := entry.(map[string]any)
|
|
1450
|
-
if !ok {
|
|
1451
|
-
kept = append(kept, entry)
|
|
1452
|
-
continue
|
|
1453
|
-
}
|
|
1454
|
-
commands, ok := entryMap["hooks"].([]any)
|
|
1455
|
-
if !ok {
|
|
1456
|
-
kept = append(kept, entry)
|
|
1457
|
-
continue
|
|
1458
|
-
}
|
|
1459
|
-
var keptCommands []any
|
|
1460
|
-
for _, command := range commands {
|
|
1461
|
-
if !isDevritesHookCommand(command) {
|
|
1462
|
-
keptCommands = append(keptCommands, command)
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
if len(keptCommands) > 0 {
|
|
1466
|
-
nextEntry := make(map[string]any, len(entryMap))
|
|
1467
|
-
for k, v := range entryMap {
|
|
1468
|
-
nextEntry[k] = v
|
|
1469
|
-
}
|
|
1470
|
-
nextEntry["hooks"] = keptCommands
|
|
1471
|
-
kept = append(kept, nextEntry)
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
if len(kept) > 0 {
|
|
1475
|
-
hooks[event] = kept
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
if len(hooks) == 0 {
|
|
1479
|
-
delete(next, "hooks")
|
|
1480
|
-
} else {
|
|
1481
|
-
next["hooks"] = hooks
|
|
1482
|
-
}
|
|
1483
|
-
return next
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
func stripDevritesSettings(config map[string]any, removeOwnedDefaultMode bool) map[string]any {
|
|
1487
|
-
next := stripDevritesHooks(config)
|
|
1488
|
-
if comment, ok := next["$comment"].(string); ok && isDevritesSettingsComment(comment) {
|
|
1489
|
-
delete(next, "$comment")
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
rawPermissions, exists := next["permissions"]
|
|
1493
|
-
if !exists {
|
|
1494
|
-
return next
|
|
1495
|
-
}
|
|
1496
|
-
permissions, ok := rawPermissions.(map[string]any)
|
|
1497
|
-
if !ok {
|
|
1498
|
-
return next
|
|
1499
|
-
}
|
|
1500
|
-
clean := make(map[string]any, len(permissions))
|
|
1501
|
-
for key, value := range permissions {
|
|
1502
|
-
switch key {
|
|
1503
|
-
case "allow":
|
|
1504
|
-
rules, ok := value.([]any)
|
|
1505
|
-
if !ok {
|
|
1506
|
-
clean[key] = value
|
|
1507
|
-
continue
|
|
1508
|
-
}
|
|
1509
|
-
kept := make([]any, 0, len(rules))
|
|
1510
|
-
for _, rule := range rules {
|
|
1511
|
-
if !isDevritesPermissionRule(rule) {
|
|
1512
|
-
kept = append(kept, rule)
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
if len(kept) > 0 {
|
|
1516
|
-
clean[key] = kept
|
|
1517
|
-
}
|
|
1518
|
-
case "defaultMode":
|
|
1519
|
-
if removeOwnedDefaultMode && value == "plan" {
|
|
1520
|
-
continue
|
|
1521
|
-
}
|
|
1522
|
-
clean[key] = value
|
|
1523
|
-
default:
|
|
1524
|
-
clean[key] = value
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
if len(clean) == 0 {
|
|
1528
|
-
delete(next, "permissions")
|
|
1529
|
-
} else {
|
|
1530
|
-
next["permissions"] = clean
|
|
1531
|
-
}
|
|
1532
|
-
return next
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
func mergeClaudeSettingsConfig(current, desired map[string]any, defaultModeOwned bool) (map[string]any, bool, error) {
|
|
1536
|
-
next := stripDevritesSettings(current, false)
|
|
1537
|
-
permissions := map[string]any{}
|
|
1538
|
-
if raw, exists := next["permissions"]; exists {
|
|
1539
|
-
var ok bool
|
|
1540
|
-
permissions, ok = raw.(map[string]any)
|
|
1541
|
-
if !ok {
|
|
1542
|
-
return nil, false, fmt.Errorf("existing Claude permissions must be a JSON object")
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
switch mode, exists := permissions["defaultMode"]; {
|
|
1547
|
-
case !exists:
|
|
1548
|
-
defaultModeOwned = true
|
|
1549
|
-
case mode == "plan":
|
|
1550
|
-
case mode != "plan":
|
|
1551
|
-
return nil, false, fmt.Errorf("existing Claude permissions.defaultMode is %q; DevRites requires plan mode for the read-only root orchestrator", mode)
|
|
1552
|
-
}
|
|
1553
|
-
permissions["defaultMode"] = "plan"
|
|
1554
|
-
|
|
1555
|
-
desiredPermissions, ok := desired["permissions"].(map[string]any)
|
|
1556
|
-
if !ok || desiredPermissions["defaultMode"] != "plan" {
|
|
1557
|
-
return nil, false, fmt.Errorf("DevRites Claude settings payload must declare permissions.defaultMode=plan")
|
|
1558
|
-
}
|
|
1559
|
-
desiredAllow, ok := desiredPermissions["allow"].([]any)
|
|
1560
|
-
if !ok {
|
|
1561
|
-
return nil, false, fmt.Errorf("DevRites Claude settings payload permissions.allow must be an array")
|
|
1562
|
-
}
|
|
1563
|
-
var allow []any
|
|
1564
|
-
if existingAllow, exists := permissions["allow"]; exists {
|
|
1565
|
-
allow, ok = existingAllow.([]any)
|
|
1566
|
-
if !ok {
|
|
1567
|
-
return nil, false, fmt.Errorf("existing Claude permissions.allow must be an array")
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
seen := map[string]bool{}
|
|
1571
|
-
for _, rule := range allow {
|
|
1572
|
-
if value, ok := rule.(string); ok {
|
|
1573
|
-
seen[value] = true
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
for _, rule := range desiredAllow {
|
|
1577
|
-
value, ok := rule.(string)
|
|
1578
|
-
if !ok {
|
|
1579
|
-
return nil, false, fmt.Errorf("DevRites Claude permission rules must be strings")
|
|
1580
|
-
}
|
|
1581
|
-
if !seen[value] {
|
|
1582
|
-
allow = append(allow, value)
|
|
1583
|
-
seen[value] = true
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
|
-
permissions["allow"] = allow
|
|
1587
|
-
next["permissions"] = permissions
|
|
1588
|
-
if _, exists := next["$comment"]; !exists {
|
|
1589
|
-
if comment, ok := desired["$comment"].(string); ok {
|
|
1590
|
-
next["$comment"] = comment
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
return next, defaultModeOwned, nil
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
func isDevritesPermissionRule(value any) bool {
|
|
1597
|
-
rule, ok := value.(string)
|
|
1598
|
-
return ok && strings.HasPrefix(strings.TrimSpace(rule), "Bash(devrites-engine ")
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
func (r *runner) installBinary() error {
|
|
1602
|
-
if !r.opts.WithBinary || os.Getenv("DEVRITES_NO_BINARY") == "1" {
|
|
1603
|
-
fmt.Fprintln(r.opts.Stdout, " engine binary: skipped (--no-binary).")
|
|
1604
|
-
return nil
|
|
1605
|
-
}
|
|
1606
|
-
if r.opts.DryRun {
|
|
1607
|
-
fmt.Fprintln(r.opts.Stdout, " would install the global devrites-engine control-plane binary")
|
|
1608
|
-
return nil
|
|
1609
|
-
}
|
|
1610
|
-
tag := r.binaryTag()
|
|
1611
|
-
incoming := strings.TrimPrefix(tag, "v")
|
|
1612
|
-
dest := binaryDest()
|
|
1613
|
-
if exists(dest) {
|
|
1614
|
-
if ev := engineVersion(dest); semverLike(ev) && semverLike(incoming) && verGT(ev, incoming) {
|
|
1615
|
-
fmt.Fprintf(r.opts.Stderr, "warning: engine binary: installed %s is newer than %s - refusing to downgrade (kept).\n", ev, tag)
|
|
1616
|
-
return nil
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
staged, cleanup := r.preparedBinary, func() {}
|
|
1620
|
-
if staged == "" {
|
|
1621
|
-
var err error
|
|
1622
|
-
staged, cleanup, err = r.acquireBinary(tag)
|
|
1623
|
-
if err != nil {
|
|
1624
|
-
return r.binaryInstallFailure(err)
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
defer cleanup()
|
|
1628
|
-
if _, err := verifyEngineBinary(staged, tag, 30*time.Second); err != nil {
|
|
1629
|
-
return fmt.Errorf("verify staged engine binary %s: %w", staged, err)
|
|
1630
|
-
}
|
|
1631
|
-
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
|
|
1632
|
-
return err
|
|
1633
|
-
}
|
|
1634
|
-
data, err := os.ReadFile(staged)
|
|
1635
|
-
if err != nil {
|
|
1636
|
-
return err
|
|
1637
|
-
}
|
|
1638
|
-
backup, oldMode, hadOld, err := backupBinary(dest)
|
|
1639
|
-
if err != nil {
|
|
1640
|
-
return err
|
|
1641
|
-
}
|
|
1642
|
-
if backup != "" {
|
|
1643
|
-
defer os.Remove(backup)
|
|
1644
|
-
}
|
|
1645
|
-
if err := fsutil.WriteFileAtomic(dest, data, 0o755); err != nil {
|
|
1646
|
-
if restoreErr := restoreBinary(dest, backup, oldMode, hadOld); restoreErr != nil {
|
|
1647
|
-
return fmt.Errorf("install binary: %v; restore previous binary: %w", err, restoreErr)
|
|
1648
|
-
}
|
|
1649
|
-
return err
|
|
1650
|
-
}
|
|
1651
|
-
_ = os.Chmod(dest, 0o755)
|
|
1652
|
-
if _, err := verifyEngineBinary(dest, tag, 30*time.Second); err != nil {
|
|
1653
|
-
if restoreErr := restoreBinary(dest, backup, oldMode, hadOld); restoreErr != nil {
|
|
1654
|
-
return fmt.Errorf("verify installed engine binary: %v; restore previous binary: %w", err, restoreErr)
|
|
1655
|
-
}
|
|
1656
|
-
if hadOld {
|
|
1657
|
-
return fmt.Errorf("verify installed engine binary: %w (previous binary restored)", err)
|
|
1658
|
-
}
|
|
1659
|
-
return fmt.Errorf("verify installed engine binary: %w (bad binary removed)", err)
|
|
1660
|
-
}
|
|
1661
|
-
fmt.Fprintf(r.opts.Stdout, " engine binary: installed %s\n", dest)
|
|
1662
|
-
return nil
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
func (r *runner) binaryTag() string {
|
|
1666
|
-
if r.requiredBinaryTag != "" {
|
|
1667
|
-
return r.requiredBinaryTag
|
|
1668
|
-
}
|
|
1669
|
-
if tag := os.Getenv("DEVRITES_REF"); semverLike(tag) {
|
|
1670
|
-
return tag
|
|
1671
|
-
}
|
|
1672
|
-
return "v" + strings.TrimPrefix(installedVersion(r.source), "v")
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
func backupBinary(dest string) (string, fs.FileMode, bool, error) {
|
|
1676
|
-
info, err := os.Lstat(dest)
|
|
1677
|
-
if os.IsNotExist(err) {
|
|
1678
|
-
return "", 0, false, nil
|
|
1679
|
-
}
|
|
1680
|
-
if err != nil {
|
|
1681
|
-
return "", 0, false, fmt.Errorf("inspect existing engine binary: %w", err)
|
|
1682
|
-
}
|
|
1683
|
-
if !info.Mode().IsRegular() {
|
|
1684
|
-
return "", 0, false, fmt.Errorf("refusing to replace non-regular engine binary: %s", dest)
|
|
1685
|
-
}
|
|
1686
|
-
data, err := os.ReadFile(dest)
|
|
1687
|
-
if err != nil {
|
|
1688
|
-
return "", 0, false, fmt.Errorf("read existing engine binary: %w", err)
|
|
1689
|
-
}
|
|
1690
|
-
f, err := os.CreateTemp(filepath.Dir(dest), "."+filepath.Base(dest)+".backup-*")
|
|
1691
|
-
if err != nil {
|
|
1692
|
-
return "", 0, false, fmt.Errorf("create engine binary backup: %w", err)
|
|
1693
|
-
}
|
|
1694
|
-
backup := f.Name()
|
|
1695
|
-
if err := f.Close(); err != nil {
|
|
1696
|
-
_ = os.Remove(backup)
|
|
1697
|
-
return "", 0, false, fmt.Errorf("create engine binary backup: %w", err)
|
|
1698
|
-
}
|
|
1699
|
-
if err := fsutil.WriteFileAtomic(backup, data, info.Mode().Perm()); err != nil {
|
|
1700
|
-
_ = os.Remove(backup)
|
|
1701
|
-
return "", 0, false, fmt.Errorf("write engine binary backup: %w", err)
|
|
1702
|
-
}
|
|
1703
|
-
return backup, info.Mode().Perm(), true, nil
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
func restoreBinary(dest, backup string, mode fs.FileMode, hadOld bool) error {
|
|
1707
|
-
if !hadOld {
|
|
1708
|
-
if err := os.Remove(dest); err != nil && !os.IsNotExist(err) {
|
|
1709
|
-
return err
|
|
1710
|
-
}
|
|
1711
|
-
return nil
|
|
1712
|
-
}
|
|
1713
|
-
data, err := os.ReadFile(backup)
|
|
1714
|
-
if err != nil {
|
|
1715
|
-
return err
|
|
1716
|
-
}
|
|
1717
|
-
return fsutil.WriteFileAtomic(dest, data, mode)
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
func (r *runner) binaryInstallFailure(err error) error {
|
|
1721
|
-
if r.preparedBinary != "" {
|
|
1722
|
-
return err
|
|
1723
|
-
}
|
|
1724
|
-
if os.Getenv("DEVRITES_ENGINE_CLI") != "" {
|
|
1725
|
-
return err
|
|
1726
|
-
}
|
|
1727
|
-
fmt.Fprintf(r.opts.Stderr, "warning: engine binary: %v; continuing without it.\n", err)
|
|
1728
|
-
return nil
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
func (r *runner) acquireBinary(tag string) (string, func(), error) {
|
|
1732
|
-
path := os.Getenv("DEVRITES_ENGINE_CLI")
|
|
1733
|
-
if path == "" {
|
|
1734
|
-
return "", func() {}, fmt.Errorf("no DEVRITES_ENGINE_CLI handoff")
|
|
1735
|
-
}
|
|
1736
|
-
if !exists(path) {
|
|
1737
|
-
return "", func() {}, fmt.Errorf("DEVRITES_ENGINE_CLI points to a missing binary: %s", path)
|
|
1738
|
-
}
|
|
1739
|
-
if _, err := verifyEngineBinary(path, tag, 30*time.Second); err != nil {
|
|
1740
|
-
return "", func() {}, fmt.Errorf("DEVRITES_ENGINE_CLI is incompatible: %w", err)
|
|
1741
|
-
}
|
|
1742
|
-
return path, func() {}, nil
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
func (r *runner) removeBinary() error {
|
|
1746
|
-
if r.opts.KeepBinary {
|
|
1747
|
-
fmt.Fprintln(r.opts.Stdout, " kept the global devrites-engine binary (--keep-binary).")
|
|
1748
|
-
return nil
|
|
1749
|
-
}
|
|
1750
|
-
for _, p := range binaryCandidates() {
|
|
1751
|
-
if p == "" || !exists(p) {
|
|
1752
|
-
continue
|
|
1753
|
-
}
|
|
1754
|
-
if r.opts.DryRun {
|
|
1755
|
-
fmt.Fprintf(r.opts.Stdout, " [remove] %s (global engine binary)\n", p)
|
|
1756
|
-
continue
|
|
1757
|
-
}
|
|
1758
|
-
if err := os.Remove(p); err == nil {
|
|
1759
|
-
fmt.Fprintf(r.opts.Stdout, " [remove] %s\n", p)
|
|
1760
|
-
return nil
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
return nil
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
func binaryDest() string {
|
|
1767
|
-
if dir := os.Getenv("DEVRITES_BIN_DIR"); dir != "" {
|
|
1768
|
-
return filepath.Join(dir, engineBinaryName())
|
|
1769
|
-
}
|
|
1770
|
-
if home, err := os.UserHomeDir(); err == nil {
|
|
1771
|
-
return filepath.Join(home, ".local", "bin", engineBinaryName())
|
|
1772
|
-
}
|
|
1773
|
-
return filepath.Join("/usr/local/bin", engineBinaryName())
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
func engineBinaryName() string {
|
|
1777
|
-
if runtime.GOOS == "windows" {
|
|
1778
|
-
return "devrites-engine.exe"
|
|
1779
|
-
}
|
|
1780
|
-
return "devrites-engine"
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
func binaryCandidates() []string {
|
|
1784
|
-
candidates := []string{}
|
|
1785
|
-
if dir := os.Getenv("DEVRITES_BIN_DIR"); dir != "" {
|
|
1786
|
-
candidates = append(candidates, filepath.Join(dir, engineBinaryName()))
|
|
1787
|
-
}
|
|
1788
|
-
home, _ := os.UserHomeDir()
|
|
1789
|
-
if home != "" {
|
|
1790
|
-
candidates = append(candidates, filepath.Join(home, ".local", "bin", engineBinaryName()))
|
|
1791
|
-
}
|
|
1792
|
-
return append(candidates, filepath.Join("/usr/local/bin", engineBinaryName()))
|
|
1793
|
-
}
|
|
1794
|
-
|
|
1795
|
-
func engineVersion(path string) string {
|
|
1796
|
-
got, err := readEngineVersion(path, 30*time.Second)
|
|
1797
|
-
if err != nil {
|
|
1798
|
-
return ""
|
|
1799
|
-
}
|
|
1800
|
-
return got
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
|
-
func verifyEngineBinary(path, want string, timeout time.Duration) (string, error) {
|
|
1804
|
-
want = strings.TrimPrefix(strings.TrimSpace(want), "v")
|
|
1805
|
-
if want == "" || want == "dev" || !semverLike(want) {
|
|
1806
|
-
return "", fmt.Errorf("invalid requested version %q", want)
|
|
1807
|
-
}
|
|
1808
|
-
got, err := readEngineVersion(path, timeout)
|
|
1809
|
-
if err != nil {
|
|
1810
|
-
return "", err
|
|
1811
|
-
}
|
|
1812
|
-
if strings.TrimPrefix(got, "v") != want {
|
|
1813
|
-
return "", fmt.Errorf("version mismatch: got %s want %s", got, want)
|
|
1814
|
-
}
|
|
1815
|
-
return got, nil
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
|
-
func readEngineVersion(path string, timeout time.Duration) (string, error) {
|
|
1819
|
-
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
1820
|
-
defer cancel()
|
|
1821
|
-
out, err := exec.CommandContext(ctx, path, "version").Output()
|
|
1822
|
-
if err != nil {
|
|
1823
|
-
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
|
1824
|
-
return "", fmt.Errorf("version command timed out after %s", timeout)
|
|
1825
|
-
}
|
|
1826
|
-
return "", fmt.Errorf("run exact path %s version: %w", path, err)
|
|
1827
|
-
}
|
|
1828
|
-
line := strings.TrimSuffix(string(out), "\n")
|
|
1829
|
-
line = strings.TrimSuffix(line, "\r")
|
|
1830
|
-
if strings.ContainsAny(line, "\r\n") {
|
|
1831
|
-
return "", fmt.Errorf("invalid multi-line version output")
|
|
1832
|
-
}
|
|
1833
|
-
line = strings.TrimSpace(line)
|
|
1834
|
-
if line == "" || line == "dev" || !semverLike(line) {
|
|
1835
|
-
return "", fmt.Errorf("invalid version output %q", line)
|
|
1836
|
-
}
|
|
1837
|
-
return line, nil
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
func readManifest(path string) (map[string]managedRecord, error) {
|
|
1841
|
-
records, _, err := parseManifest(path)
|
|
1842
|
-
return records, err
|
|
1843
|
-
}
|
|
1844
|
-
|
|
1845
|
-
func readManifestList(path string) ([]string, error) {
|
|
1846
|
-
_, entries, err := parseManifest(path)
|
|
1847
|
-
return entries, err
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
|
-
func parseManifest(path string) (map[string]managedRecord, []string, error) {
|
|
1851
|
-
data, err := os.ReadFile(path)
|
|
1852
|
-
if err != nil {
|
|
1853
|
-
if errors.Is(err, fs.ErrNotExist) {
|
|
1854
|
-
return map[string]managedRecord{}, nil, nil
|
|
1855
|
-
}
|
|
1856
|
-
return nil, nil, fmt.Errorf("read manifest %s: %w", path, err)
|
|
1857
|
-
}
|
|
1858
|
-
hashes := map[string]string{}
|
|
1859
|
-
var out []string
|
|
1860
|
-
for _, line := range strings.Split(string(data), "\n") {
|
|
1861
|
-
line = strings.TrimSpace(line)
|
|
1862
|
-
if strings.HasPrefix(line, "# managed: ") {
|
|
1863
|
-
record := strings.TrimPrefix(line, "# managed: ")
|
|
1864
|
-
i := strings.LastIndex(record, " sha256:")
|
|
1865
|
-
if i > 0 {
|
|
1866
|
-
rel := strings.TrimSpace(record[:i])
|
|
1867
|
-
hash := "sha256:" + strings.ToLower(strings.TrimSpace(record[i+len(" sha256:"):]))
|
|
1868
|
-
if filepath.IsLocal(filepath.FromSlash(rel)) && validManagedHash(hash) {
|
|
1869
|
-
hashes[rel] = hash
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
continue
|
|
1873
|
-
}
|
|
1874
|
-
if line == "" || strings.HasPrefix(line, "#") {
|
|
1875
|
-
continue
|
|
1876
|
-
}
|
|
1877
|
-
// The manifest is hand-editable; an entry that escapes the target
|
|
1878
|
-
// (absolute or ..-traversal) must never be overwritten or removed.
|
|
1879
|
-
if !filepath.IsLocal(filepath.FromSlash(line)) {
|
|
1880
|
-
continue
|
|
1881
|
-
}
|
|
1882
|
-
out = append(out, line)
|
|
1883
|
-
}
|
|
1884
|
-
records := make(map[string]managedRecord, len(out))
|
|
1885
|
-
for _, rel := range out {
|
|
1886
|
-
records[rel] = managedRecord{Hash: hashes[rel]}
|
|
1887
|
-
}
|
|
1888
|
-
return records, out, nil
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
func validManagedHash(hash string) bool {
|
|
1892
|
-
const prefix = "sha256:"
|
|
1893
|
-
if !strings.HasPrefix(hash, prefix) || len(hash) != len(prefix)+sha256.Size*2 {
|
|
1894
|
-
return false
|
|
1895
|
-
}
|
|
1896
|
-
_, err := hex.DecodeString(strings.TrimPrefix(hash, prefix))
|
|
1897
|
-
return err == nil
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
func manifestHeader(path, key string) string {
|
|
1901
|
-
data, err := os.ReadFile(path)
|
|
1902
|
-
if err != nil {
|
|
1903
|
-
return ""
|
|
1904
|
-
}
|
|
1905
|
-
prefix := "# " + key + ":"
|
|
1906
|
-
for _, line := range strings.Split(string(data), "\n") {
|
|
1907
|
-
if strings.HasPrefix(line, prefix) {
|
|
1908
|
-
return strings.TrimSpace(strings.TrimPrefix(line, prefix))
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
1911
|
-
return ""
|
|
1912
|
-
}
|
|
1913
|
-
|
|
1914
|
-
func installedVersion(source string) string {
|
|
1915
|
-
if source != "" {
|
|
1916
|
-
data, err := os.ReadFile(filepath.Join(source, "package.json"))
|
|
1917
|
-
if err != nil {
|
|
1918
|
-
return "unknown"
|
|
1919
|
-
}
|
|
1920
|
-
var pkg struct {
|
|
1921
|
-
Version string `json:"version"`
|
|
1922
|
-
}
|
|
1923
|
-
if json.Unmarshal(data, &pkg) != nil || pkg.Version == "" {
|
|
1924
|
-
return "unknown"
|
|
1925
|
-
}
|
|
1926
|
-
return pkg.Version
|
|
1927
|
-
}
|
|
1928
|
-
if version.Version != "" && version.Version != "dev" {
|
|
1929
|
-
return strings.TrimPrefix(version.Version, "v")
|
|
1930
|
-
}
|
|
1931
|
-
return "unknown"
|
|
1932
|
-
}
|
|
1933
|
-
|
|
1934
338
|
func isGlobalTarget(target string) bool {
|
|
1935
339
|
home, _ := os.UserHomeDir()
|
|
1936
340
|
if target == home {
|