devrites 3.0.6 → 3.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +4 -2
- package/engine/internal/install/install.go +93 -24
- package/engine/internal/install/install_test.go +108 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to DevRites are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and DevRites adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Releases are generated automatically by [semantic-release](https://semantic-release.gitbook.io/) from Conventional Commits on `main`.
|
|
4
4
|
|
|
5
|
+
## [3.0.7](https://github.com/ViktorsBaikers/DevRites/compare/v3.0.6...v3.0.7) (2026-07-22)
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
* **installer:** harden binary acquisition ([7534198](https://github.com/ViktorsBaikers/DevRites/commit/7534198dd3f6a29f065fe715e763e02bdc8a71c0))
|
|
10
|
+
|
|
5
11
|
## [3.0.6](https://github.com/ViktorsBaikers/DevRites/compare/v3.0.5...v3.0.6) (2026-07-22)
|
|
6
12
|
|
|
7
13
|
### Fixed
|
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ final commit, push, and tag, and it requires a typed `GO` confirmation.
|
|
|
21
21
|
Unattended runs may create local WIP checkpoint commits along the way, but only
|
|
22
22
|
Ship collapses and pushes them.
|
|
23
23
|
|
|
24
|
-
**Status:** [`v3.0.
|
|
24
|
+
**Status:** [`v3.0.7`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v3.0.7): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
|
|
25
25
|
|
|
26
26
|
## Quick start
|
|
27
27
|
|
|
@@ -297,7 +297,9 @@ configured engine or a local `engine/devrites-engine`, can download a
|
|
|
297
297
|
checksummed release binary, can build a temporary engine from the local Go
|
|
298
298
|
source, and finally falls back to an existing `devrites-engine` on `PATH`. Use `--no-binary` or
|
|
299
299
|
`DEVRITES_NO_BINARY=1` if you do not want the installer to keep the shared
|
|
300
|
-
binary outside the project.
|
|
300
|
+
binary outside the project. `devrites-engine update` also prefers the
|
|
301
|
+
checksummed platform release binary and only builds from source as a fallback,
|
|
302
|
+
without reading Git metadata from the target project.
|
|
301
303
|
|
|
302
304
|
Workflow state is read through the engine rather than shell injection. The
|
|
303
305
|
installer never writes `defaultMode: bypassPermissions`. Skills use networked
|
|
@@ -16,6 +16,7 @@ import (
|
|
|
16
16
|
"os/exec"
|
|
17
17
|
"path/filepath"
|
|
18
18
|
"reflect"
|
|
19
|
+
"runtime"
|
|
19
20
|
"slices"
|
|
20
21
|
"sort"
|
|
21
22
|
"strings"
|
|
@@ -30,6 +31,7 @@ import (
|
|
|
30
31
|
|
|
31
32
|
const ManifestName = devritespaths.ManifestName
|
|
32
33
|
const defaultRepo = "ViktorsBaikers/DevRites"
|
|
34
|
+
const githubBaseURL = "https://github.com"
|
|
33
35
|
|
|
34
36
|
type Mode string
|
|
35
37
|
|
|
@@ -69,15 +71,17 @@ type stats struct {
|
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
type runner struct {
|
|
72
|
-
opts
|
|
73
|
-
target
|
|
74
|
-
payload
|
|
75
|
-
payloadFS
|
|
76
|
-
source
|
|
77
|
-
manifest
|
|
78
|
-
records
|
|
79
|
-
prev
|
|
80
|
-
stats
|
|
74
|
+
opts Options
|
|
75
|
+
target string
|
|
76
|
+
payload string
|
|
77
|
+
payloadFS fs.FS
|
|
78
|
+
source string
|
|
79
|
+
manifest []string
|
|
80
|
+
records map[string]string
|
|
81
|
+
prev map[string]bool
|
|
82
|
+
stats stats
|
|
83
|
+
releaseBinaryTag string
|
|
84
|
+
preparedBinary string
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
func DefaultOptions(mode Mode) Options {
|
|
@@ -849,7 +853,20 @@ func runUpdate(opts Options) error {
|
|
|
849
853
|
if opts.DryRun {
|
|
850
854
|
next.DryRun = true
|
|
851
855
|
}
|
|
852
|
-
|
|
856
|
+
r, err := newRunner(next)
|
|
857
|
+
if err != nil {
|
|
858
|
+
return fmt.Errorf("prepare installer: %w", err)
|
|
859
|
+
}
|
|
860
|
+
r.releaseBinaryTag = latestTag
|
|
861
|
+
if next.WithBinary && os.Getenv("DEVRITES_NO_BINARY") != "1" && !next.DryRun {
|
|
862
|
+
staged, cleanup, err := r.acquireBinary(latestTag, githubBaseURL)
|
|
863
|
+
if err != nil {
|
|
864
|
+
return fmt.Errorf("prepare engine binary: %w", err)
|
|
865
|
+
}
|
|
866
|
+
defer cleanup()
|
|
867
|
+
r.preparedBinary = staged
|
|
868
|
+
}
|
|
869
|
+
return r.install()
|
|
853
870
|
}
|
|
854
871
|
|
|
855
872
|
func resolveUpdateTag(opts Options) (string, error) {
|
|
@@ -1297,24 +1314,24 @@ func (r *runner) installBinary() error {
|
|
|
1297
1314
|
return nil
|
|
1298
1315
|
}
|
|
1299
1316
|
}
|
|
1300
|
-
staged, cleanup
|
|
1301
|
-
if
|
|
1302
|
-
|
|
1303
|
-
|
|
1317
|
+
staged, cleanup := r.preparedBinary, func() {}
|
|
1318
|
+
if staged == "" {
|
|
1319
|
+
var err error
|
|
1320
|
+
staged, cleanup, err = r.acquireBinary(tag, githubBaseURL)
|
|
1321
|
+
if err != nil {
|
|
1322
|
+
return r.binaryInstallFailure(err)
|
|
1323
|
+
}
|
|
1304
1324
|
}
|
|
1305
1325
|
defer cleanup()
|
|
1306
1326
|
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
|
|
1307
|
-
|
|
1308
|
-
return nil
|
|
1327
|
+
return r.binaryInstallFailure(err)
|
|
1309
1328
|
}
|
|
1310
1329
|
data, err := os.ReadFile(staged)
|
|
1311
1330
|
if err != nil {
|
|
1312
|
-
|
|
1313
|
-
return nil
|
|
1331
|
+
return r.binaryInstallFailure(err)
|
|
1314
1332
|
}
|
|
1315
1333
|
if err := fsutil.WriteFileAtomic(dest, data, 0o755); err != nil {
|
|
1316
|
-
|
|
1317
|
-
return nil
|
|
1334
|
+
return r.binaryInstallFailure(err)
|
|
1318
1335
|
}
|
|
1319
1336
|
_ = os.Chmod(dest, 0o755)
|
|
1320
1337
|
fmt.Fprintf(r.opts.Stdout, " engine binary: installed %s\n", dest)
|
|
@@ -1324,12 +1341,23 @@ func (r *runner) installBinary() error {
|
|
|
1324
1341
|
return nil
|
|
1325
1342
|
}
|
|
1326
1343
|
|
|
1327
|
-
func (r *runner)
|
|
1344
|
+
func (r *runner) binaryInstallFailure(err error) error {
|
|
1345
|
+
if r.preparedBinary != "" {
|
|
1346
|
+
return err
|
|
1347
|
+
}
|
|
1348
|
+
fmt.Fprintf(r.opts.Stderr, "warning: engine binary: %v - hooks fail open until you install it.\n", err)
|
|
1349
|
+
return nil
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
func (r *runner) acquireBinary(tag, baseURL string) (string, func(), error) {
|
|
1328
1353
|
if v := os.Getenv("DEVRITES_ENGINE_CLI"); v != "" {
|
|
1329
1354
|
if exists(v) {
|
|
1330
|
-
|
|
1355
|
+
if r.releaseBinaryTag == "" || strings.TrimPrefix(engineVersion(v), "v") == strings.TrimPrefix(r.releaseBinaryTag, "v") {
|
|
1356
|
+
return v, func() {}, nil
|
|
1357
|
+
}
|
|
1358
|
+
} else {
|
|
1359
|
+
return "", func() {}, fmt.Errorf("DEVRITES_ENGINE_CLI points to a missing binary: %s", v)
|
|
1331
1360
|
}
|
|
1332
|
-
return "", func() {}, fmt.Errorf("DEVRITES_ENGINE_CLI points to a missing binary: %s", v)
|
|
1333
1361
|
}
|
|
1334
1362
|
tmp, err := os.MkdirTemp("", "devrites-engine-*")
|
|
1335
1363
|
if err != nil {
|
|
@@ -1337,19 +1365,60 @@ func (r *runner) acquireBinary(tag string) (string, func(), error) {
|
|
|
1337
1365
|
}
|
|
1338
1366
|
cleanup := func() { _ = os.RemoveAll(tmp) }
|
|
1339
1367
|
staged := filepath.Join(tmp, "devrites-engine")
|
|
1368
|
+
var releaseErr error
|
|
1369
|
+
if r.releaseBinaryTag != "" && os.Getenv("DEVRITES_UPDATE_BUNDLE") == "" {
|
|
1370
|
+
repo := os.Getenv("DEVRITES_REPO")
|
|
1371
|
+
if repo == "" {
|
|
1372
|
+
repo = defaultRepo
|
|
1373
|
+
}
|
|
1374
|
+
downloaded, err := downloadReleaseBinary(baseURL, repo, r.releaseBinaryTag, staged)
|
|
1375
|
+
if err == nil {
|
|
1376
|
+
return staged, cleanup, nil
|
|
1377
|
+
}
|
|
1378
|
+
if downloaded {
|
|
1379
|
+
return "", cleanup, err
|
|
1380
|
+
}
|
|
1381
|
+
releaseErr = err
|
|
1382
|
+
}
|
|
1340
1383
|
if r.source != "" && exists(filepath.Join(r.source, "engine", "go.mod")) {
|
|
1341
1384
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
|
1342
1385
|
defer cancel()
|
|
1343
|
-
cmd := exec.CommandContext(ctx, "go", "build", "-trimpath", "-ldflags", "-s -w -X github.com/devrites/devrites/internal/version.Version="+tag, "-o", staged, ".")
|
|
1386
|
+
cmd := exec.CommandContext(ctx, "go", "build", "-buildvcs=false", "-trimpath", "-ldflags", "-s -w -X github.com/devrites/devrites/internal/version.Version="+tag, "-o", staged, ".")
|
|
1344
1387
|
cmd.Dir = filepath.Join(r.source, "engine")
|
|
1345
1388
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
1389
|
+
if releaseErr != nil {
|
|
1390
|
+
return "", cleanup, fmt.Errorf("could not download release binary: %v; source fallback failed: %w: %s", releaseErr, err, strings.TrimSpace(string(out)))
|
|
1391
|
+
}
|
|
1346
1392
|
return "", cleanup, fmt.Errorf("could not build from source: %w: %s", err, strings.TrimSpace(string(out)))
|
|
1347
1393
|
}
|
|
1348
1394
|
return staged, cleanup, nil
|
|
1349
1395
|
}
|
|
1396
|
+
if releaseErr != nil {
|
|
1397
|
+
return "", cleanup, fmt.Errorf("could not obtain a binary: %v; no Go source fallback", releaseErr)
|
|
1398
|
+
}
|
|
1350
1399
|
return "", cleanup, fmt.Errorf("could not obtain a binary (no DEVRITES_ENGINE_CLI handoff, no Go source)")
|
|
1351
1400
|
}
|
|
1352
1401
|
|
|
1402
|
+
func downloadReleaseBinary(baseURL, repo, tag, dest string) (bool, error) {
|
|
1403
|
+
asset := fmt.Sprintf("devrites-%s-%s", runtime.GOOS, runtime.GOARCH)
|
|
1404
|
+
if runtime.GOOS == "windows" {
|
|
1405
|
+
asset += ".exe"
|
|
1406
|
+
}
|
|
1407
|
+
url := fmt.Sprintf("%s/%s/releases/download/%s/%s", strings.TrimRight(baseURL, "/"), repo, tag, asset)
|
|
1408
|
+
if err := iohooks.DownloadFile(url, dest); err != nil {
|
|
1409
|
+
return false, fmt.Errorf("download %s: %w", asset, err)
|
|
1410
|
+
}
|
|
1411
|
+
sumPath := dest + ".sha256"
|
|
1412
|
+
defer os.Remove(sumPath)
|
|
1413
|
+
if err := iohooks.DownloadFile(url+".sha256", sumPath); err != nil {
|
|
1414
|
+
return true, fmt.Errorf("could not verify release binary %s: missing checksum: %w", asset, err)
|
|
1415
|
+
}
|
|
1416
|
+
if err := verifySHA256(dest, sumPath); err != nil {
|
|
1417
|
+
return true, fmt.Errorf("could not verify release binary %s: %w", asset, err)
|
|
1418
|
+
}
|
|
1419
|
+
return true, nil
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1353
1422
|
func (r *runner) removeBinary() error {
|
|
1354
1423
|
if r.opts.KeepBinary {
|
|
1355
1424
|
fmt.Fprintln(r.opts.Stdout, " kept the global devrites-engine binary (--keep-binary).")
|
|
@@ -4,9 +4,12 @@ import (
|
|
|
4
4
|
"archive/tar"
|
|
5
5
|
"bytes"
|
|
6
6
|
"compress/gzip"
|
|
7
|
+
"crypto/sha256"
|
|
7
8
|
"encoding/json"
|
|
8
9
|
"fmt"
|
|
9
10
|
"io/fs"
|
|
11
|
+
"net/http"
|
|
12
|
+
"net/http/httptest"
|
|
10
13
|
"os"
|
|
11
14
|
"path/filepath"
|
|
12
15
|
"runtime"
|
|
@@ -347,6 +350,111 @@ func TestInstallBinaryUsesEngineHandoff(t *testing.T) {
|
|
|
347
350
|
}
|
|
348
351
|
}
|
|
349
352
|
|
|
353
|
+
func TestAcquireBinaryBuildsWithoutVCSMetadata(t *testing.T) {
|
|
354
|
+
t.Setenv("DEVRITES_ENGINE_CLI", "")
|
|
355
|
+
t.Setenv("DEVRITES_UPDATE_BUNDLE", "")
|
|
356
|
+
t.Setenv("GOFLAGS", "-buildvcs=true")
|
|
357
|
+
source := t.TempDir()
|
|
358
|
+
if err := os.Mkdir(filepath.Join(source, ".git"), 0o755); err != nil {
|
|
359
|
+
t.Fatal(err)
|
|
360
|
+
}
|
|
361
|
+
testutil.WriteFile(t, filepath.Join(source, "engine", "go.mod"), "module github.com/devrites/devrites\n\ngo 1.23\n")
|
|
362
|
+
testutil.WriteFile(t, filepath.Join(source, "engine", "main.go"), "package main\nfunc main() {}\n")
|
|
363
|
+
|
|
364
|
+
r := runner{source: source}
|
|
365
|
+
staged, cleanup, err := r.acquireBinary("v1.2.3", githubBaseURL)
|
|
366
|
+
defer cleanup()
|
|
367
|
+
if err != nil {
|
|
368
|
+
t.Fatalf("acquireBinary with unavailable VCS metadata: %v", err)
|
|
369
|
+
}
|
|
370
|
+
if !exists(staged) {
|
|
371
|
+
t.Fatalf("acquireBinary did not build %s", staged)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
func TestAcquireBinaryPrefersVerifiedReleaseOverStaleHandoff(t *testing.T) {
|
|
376
|
+
stale := filepath.Join(t.TempDir(), "devrites-engine")
|
|
377
|
+
testutil.WriteExecutable(t, stale, "#!/bin/sh\nif [ \"$1\" = version ]; then echo v1.2.2; fi\n")
|
|
378
|
+
t.Setenv("DEVRITES_ENGINE_CLI", stale)
|
|
379
|
+
t.Setenv("DEVRITES_UPDATE_BUNDLE", "")
|
|
380
|
+
t.Setenv("DEVRITES_REPO", "owner/repo")
|
|
381
|
+
tag := "v1.2.3"
|
|
382
|
+
asset := fmt.Sprintf("devrites-%s-%s", runtime.GOOS, runtime.GOARCH)
|
|
383
|
+
if runtime.GOOS == "windows" {
|
|
384
|
+
asset += ".exe"
|
|
385
|
+
}
|
|
386
|
+
path := "/owner/repo/releases/download/" + tag + "/" + asset
|
|
387
|
+
body := []byte("verified release binary")
|
|
388
|
+
sum := fmt.Sprintf("%x %s\n", sha256.Sum256(body), asset)
|
|
389
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
390
|
+
switch req.URL.Path {
|
|
391
|
+
case path:
|
|
392
|
+
_, _ = w.Write(body)
|
|
393
|
+
case path + ".sha256":
|
|
394
|
+
_, _ = w.Write([]byte(sum))
|
|
395
|
+
default:
|
|
396
|
+
http.NotFound(w, req)
|
|
397
|
+
}
|
|
398
|
+
}))
|
|
399
|
+
defer server.Close()
|
|
400
|
+
|
|
401
|
+
r := runner{releaseBinaryTag: tag}
|
|
402
|
+
staged, cleanup, err := r.acquireBinary(tag, server.URL)
|
|
403
|
+
defer cleanup()
|
|
404
|
+
if err != nil {
|
|
405
|
+
t.Fatalf("acquire verified release binary: %v", err)
|
|
406
|
+
}
|
|
407
|
+
got, err := os.ReadFile(staged)
|
|
408
|
+
if err != nil {
|
|
409
|
+
t.Fatal(err)
|
|
410
|
+
}
|
|
411
|
+
if !bytes.Equal(got, body) {
|
|
412
|
+
t.Fatalf("acquired binary = %q, want %q", got, body)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
func TestAcquireBinaryRejectsReleaseChecksumMismatch(t *testing.T) {
|
|
417
|
+
t.Setenv("DEVRITES_ENGINE_CLI", "")
|
|
418
|
+
t.Setenv("DEVRITES_UPDATE_BUNDLE", "")
|
|
419
|
+
t.Setenv("DEVRITES_REPO", "owner/repo")
|
|
420
|
+
tag := "v1.2.3"
|
|
421
|
+
source := t.TempDir()
|
|
422
|
+
testutil.WriteFile(t, filepath.Join(source, "engine", "go.mod"), "module github.com/devrites/devrites\n\ngo 1.23\n")
|
|
423
|
+
testutil.WriteFile(t, filepath.Join(source, "engine", "main.go"), "package main\nfunc main() {}\n")
|
|
424
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
425
|
+
if strings.HasSuffix(req.URL.Path, ".sha256") {
|
|
426
|
+
_, _ = w.Write([]byte("deadbeef\n"))
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
_, _ = w.Write([]byte("tampered release binary"))
|
|
430
|
+
}))
|
|
431
|
+
defer server.Close()
|
|
432
|
+
|
|
433
|
+
r := runner{source: source, releaseBinaryTag: tag}
|
|
434
|
+
_, cleanup, err := r.acquireBinary(tag, server.URL)
|
|
435
|
+
defer cleanup()
|
|
436
|
+
if err == nil || !strings.Contains(err.Error(), "checksum mismatch") {
|
|
437
|
+
t.Fatalf("acquireBinary error = %v, want checksum mismatch", err)
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
func TestInstallBinaryFailsWhenPreparedUpdateCannotBeWritten(t *testing.T) {
|
|
442
|
+
prepared := filepath.Join(t.TempDir(), "devrites-engine")
|
|
443
|
+
testutil.WriteExecutable(t, prepared, "#!/bin/sh\n")
|
|
444
|
+
blocked := filepath.Join(t.TempDir(), "not-a-directory")
|
|
445
|
+
testutil.WriteFile(t, blocked, "file\n")
|
|
446
|
+
t.Setenv("DEVRITES_BIN_DIR", filepath.Join(blocked, "bin"))
|
|
447
|
+
t.Setenv("DEVRITES_REF", "v1.2.3")
|
|
448
|
+
|
|
449
|
+
r := runner{
|
|
450
|
+
opts: DefaultOptions(ModeInstall),
|
|
451
|
+
preparedBinary: prepared,
|
|
452
|
+
}
|
|
453
|
+
if err := r.installBinary(); err == nil {
|
|
454
|
+
t.Fatal("installBinary accepted a prepared update it could not write")
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
350
458
|
func TestInstallBinaryWarnsWhenHooksCannotResolveEngine(t *testing.T) {
|
|
351
459
|
payload := testPayload(t)
|
|
352
460
|
target := t.TempDir()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devrites",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.7",
|
|
4
4
|
"description": "DevRites: a disciplined senior-engineer workflow pack for Claude Code and Codex",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://github.com/ViktorsBaikers/DevRites#readme",
|