diff --git a/go.mod b/go.mod index b21d1ac..c45e1ad 100644 --- a/go.mod +++ b/go.mod @@ -118,6 +118,7 @@ require ( github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/gordonklaus/ineffassign v0.2.0 // indirect github.com/gostaticanalysis/analysisutil v0.7.1 // indirect github.com/gostaticanalysis/comment v1.5.0 // indirect @@ -133,11 +134,13 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jedib0t/go-pretty/v6 v6.7.8 // indirect github.com/jgautheron/goconst v1.8.2 // indirect github.com/jingyugao/rowserrcheck v1.1.1 // indirect github.com/jjti/go-spancheck v0.6.5 // indirect github.com/julz/importas v0.2.0 // indirect github.com/kamstrup/intmap v0.5.2 // indirect + github.com/kanmu/go-sqlfmt v0.0.2-0.20200215095417-d1e63e2ee5eb // indirect github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect github.com/kisielk/errcheck v1.10.0 // indirect github.com/kkHAIKE/contextcheck v1.1.6 // indirect @@ -165,6 +168,7 @@ require ( github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mgechev/revive v1.15.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moricho/tparallel v0.3.2 // indirect github.com/mschoch/smat v0.2.0 // indirect @@ -173,8 +177,10 @@ require ( github.com/nishanths/exhaustive v0.12.0 // indirect github.com/nishanths/predeclared v0.2.2 // indirect github.com/nunnatsa/ginkgolinter v0.23.0 // indirect + github.com/openengineer/go-repl v0.0.0-00010101000000-000000000000 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.12.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect @@ -199,6 +205,7 @@ require ( github.com/sivchari/containedctx v1.0.3 // indirect github.com/sonatard/noctx v0.5.1 // indirect github.com/sourcegraph/go-diff v0.7.0 // indirect + github.com/specterops/dawgs/tools/dawgrun v0.0.0-00010101000000-000000000000 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/cobra v1.10.2 // indirect @@ -236,6 +243,7 @@ require ( golang.org/x/mod v0.34.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect + golang.org/x/term v0.39.0 // indirect golang.org/x/text v0.35.0 // indirect golang.org/x/tools v0.43.0 // indirect google.golang.org/protobuf v1.36.8 // indirect @@ -247,4 +255,12 @@ require ( mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 // indirect ) -tool github.com/golangci/golangci-lint/v2/cmd/golangci-lint +replace ( + github.com/openengineer/go-repl => ./tools/dawgrun/pkg/go-repl + github.com/specterops/dawgs/tools/dawgrun => ./tools/dawgrun +) + +tool ( + github.com/golangci/golangci-lint/v2/cmd/golangci-lint + github.com/specterops/dawgs/tools/dawgrun/cmd/dawgrun +) diff --git a/go.sum b/go.sum index 96b8e31..26d02fe 100644 --- a/go.sum +++ b/go.sum @@ -363,6 +363,8 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -454,6 +456,8 @@ github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jedib0t/go-pretty/v6 v6.7.8 h1:BVYrDy5DPBA3Qn9ICT+PokP9cvCv1KaHv2i+Hc8sr5o= +github.com/jedib0t/go-pretty/v6 v6.7.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jgautheron/goconst v1.8.2 h1:y0XF7X8CikZ93fSNT6WBTb/NElBu9IjaY7CCYQrCMX4= github.com/jgautheron/goconst v1.8.2/go.mod h1:A0oxgBCHy55NQn6sYpO7UdnA9p+h7cPtoOZUmvNIako= github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= @@ -473,6 +477,8 @@ github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ= github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY= github.com/kamstrup/intmap v0.5.2 h1:qnwBm1mh4XAnW9W9Ue9tZtTff8pS6+s6iKF6JRIV2Dk= github.com/kamstrup/intmap v0.5.2/go.mod h1:gWUVWHKzWj8xpJVFf5GC0O26bWmv3GqdnIX/LMT6Aq4= +github.com/kanmu/go-sqlfmt v0.0.2-0.20200215095417-d1e63e2ee5eb h1:Ztn62UtaXoFlHJIrH0AuNQrMhE355paIqZn3ik6bHNk= +github.com/kanmu/go-sqlfmt v0.0.2-0.20200215095417-d1e63e2ee5eb/go.mod h1:1+jKeqi65LLLs1GSNFRn4G/dkgg3TWeT6DTZLLQP2eM= github.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhExWKxD/fP6q0= github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY= github.com/kisielk/errcheck v1.10.0 h1:Lvs/YAHP24YKg08LA8oDw2z9fJVme090RAXd90S+rrw= @@ -588,6 +594,7 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/openengineer/go-terminal v0.0.0-20220304032943-93486212aca4/go.mod h1:Dx5mNI0A2naWQySM7zXOl/NT5QWs2sfvcQxq1tCbQVY= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= @@ -601,6 +608,7 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -992,6 +1000,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/go.work b/go.work new file mode 100644 index 0000000..4be5fc5 --- /dev/null +++ b/go.work @@ -0,0 +1,7 @@ +go 1.25.0 + +use ( + . + ./tools/dawgrun + ./tools/dawgrun/pkg/go-repl +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..2bbea63 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,116 @@ +cloud.google.com/go v0.121.2/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw= +cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= +cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= +github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= +github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q= +github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.24.2/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cristalhq/acmd v0.12.0/go.mod h1:LG5oa43pE/BbxtfMoImHCQN++0Su7dzipdgBjMCBVDQ= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mdempsky/unconvert v0.0.0-20250216222326-4a038b3d31f5/go.mod h1:mVCHGHs8r8jnrZ2ammcv8ySbhG2+rEPXegFmdNA51GI= +github.com/mgechev/dots v1.0.0/go.mod h1:rykuMydC9t3wfkM+ccYH3U3ss03vZGg6h3hmOznXLH0= +github.com/mozilla/tls-observatory v0.0.0-20250923143331-eef96233227e/go.mod h1:FUqVoUPHSEdDR0MnFM3Dh8AU0pZHLXUD127SAJGER/s= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/openai/openai-go/v3 v3.26.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= +github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7FXDQlpCiw2j81fOmAwQLnZnLGXVKUzeKQXIAw= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/quasilyte/go-ruleguard/rules v0.0.0-20211022131956-028d6511ab71/go.mod h1:4cgAphtvu7Ftv7vOT2ZOYhC6CvBxZixcasr8qIOTA50= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= +github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= +github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/quicktemplate v1.8.0/go.mod h1:qIqW8/igXt8fdrUln5kOSb+KWMaJ4Y8QUsfd1k6L2jM= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= +go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= +go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genai v1.49.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= +google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= diff --git a/tools/dawgrun/.gitignore b/tools/dawgrun/.gitignore new file mode 100644 index 0000000..6d7d302 --- /dev/null +++ b/tools/dawgrun/.gitignore @@ -0,0 +1,2 @@ +.build.env +dawgrun diff --git a/tools/dawgrun/README.md b/tools/dawgrun/README.md new file mode 100644 index 0000000..df683bf --- /dev/null +++ b/tools/dawgrun/README.md @@ -0,0 +1,193 @@ +``` + .--~~,__ + :-....,-------`~~'._.' + `-,,, ,_ ;'~U' + _,-' ,'`-__; '--. + (_/'~~ ''''(; + + ,---. ,-.-. _,---. + _,..---._ .--.' \ ,-..-.-./ \==\ _.='.'-, \ +/==/, - \ \==\-/\ \ |, \=/\=|- |==|/==.'- / +|==| _ _\/==/-|_\ ||- |/ |/ , /==/==/ - .-' +|==| .=. |\==\, - \\, , _|==|==|_ /_,-. +|==|,| | -|/==/ - ,|| - - , |==|==| , \_.' ) +|==| '=' /==/- /\ - \\ , - /==/\==\- , ( +|==|-, _`/\==\ _.\=\.-'|- /\ /==/ /==/ _ , / +`-.`.____.' `--` `--` `--` `--`------' + .-._ + .-.,.---. .--.-. .-.-./==/ \ .-._ + /==/ ` \/==/ -|/=/ ||==|, \/ /, / + |==|-, .=., |==| ,||=| -||==|- \| | + |==| '=' /==|- | =/ ||==| , | -| + |==|- , .'|==|, \/ - ||==| - _ | + |==|_ . ,'.|==|- , /|==| /\ , | + /==/ /\ , )==/ , _ .' /==/, | |- | + `--`-`--`--'`--`..---' `--`./ `--` +``` + +`dawgrun` is a work-in-progress developer tool for interacting with +`DAWGS` and the data structures it produces. + +It currently runs as a REPL for introspecting a `DAWGS`-compatible +Postgres graph, parsing and translating Cypher queries, and executing +queries against a live connection. + +## Building + +From a `DAWGS` checkout: + + go tool dawgrun + +With a customized `DAWGS` clone, for testing features, version differences, etc: + + cd tools/dawgrun + just build-with-dawgs path/to/DAWGS + +To switch the build back to mainline: + + cd tools/dawgrun + just build-with-upstream + +## Running + +You are dropped into a prompt: + + dawgrun > + +At any time, run `help` to list commands or `help ` for +detailed usage, flag defaults, and description. + +## Commands + +The REPL supports command-name completion with `Tab`; ambiguous matches render a transient popover list near the prompt and can be dismissed with `Esc`. + +Available commands: + +``` + exit Quit + explain-psql Explains a translated query over an active PG connection + help This help message, but also more detailed help for individual commands + load-db-kinds Loads/shows the kind mapping from the specified DB into the 'active set' + lookup-kind Looks up a kind from database based on kind name + lookup-kind-id Looks up a kind from database based on kind ID + open-pg-db Connects to a specified DAWGS-compatible Postgres DB to do graph introspection. + parse Parses and dumps a Cypher query to AST form. + query-cypher Executes a Cypher query and renders table or JSON output + quit Quit + runtime-trace Manage runtime tracing + translate-psql Parses a query and converts it to the underlying PostgreSQL query +``` + +### Connections and kind maps + +Most commands that touch a database take a connection _name_ as their +first argument. Names are assigned when you open the connection and +are reused for the remainder of the session. The bottom-right status +widget shows the current number of open connections. + +A "kind map" is the mapping between a graph's kind names (e.g. +`User`, `Group`) and the numeric IDs they are stored under in +Postgres. Commands that need to translate between the two +(`lookup-kind`, `lookup-kind-id`, and the `-conn` mode of +`translate-psql`) will lazily fetch a kind map from the database the +first time they need it; `load-db-kinds` forces an immediate refresh +and dumps the result. + +## Examples + +### Open a Postgres connection + + dawgrun > open-pg-db local "postgres://dawgs:dawgs@localhost:5432/dawgs?sslmode=disable" + Opened connection 'local': postgres://dawgs:dawgs@localhost:5432/dawgs?sslmode=disable + +The first argument (`local`) is the name other commands will refer +to; the second is any DAWGS-compatible Postgres connection string. + +### Inspect kinds + +Load and dump the kind mapping from a connection: + + dawgrun > load-db-kinds local + +Resolve a kind name to its numeric ID: + + dawgrun > lookup-kind local User + Kind User => 3 + +…or go the other direction: + + dawgrun > lookup-kind-id local 3 + Kind ID 3 => User + +### Parse a Cypher query to AST + + dawgrun > parse "match (n:User) where n.name = 'alice' return n" + +The REPL highlights the dumped AST as Go source. + +> **Quoting note:** the REPL splits input with shell-style rules +> (via `shlex`), so double quotes are consumed by the line parser +> before the Cypher parser ever sees them. Cypher string literals +> must use single quotes, and queries that contain them are easiest +> to pass as a single double-quoted argument, e.g. +> `"match (n) where n.name = 'alice' return n"`. + +### Translate Cypher to PostgreSQL + +Without a connection (kinds remain symbolic): + + dawgrun > translate-psql match (n:User) return n limit 10 + +With a connection, so that kind names are resolved to the IDs in the +target database: + + dawgrun > translate-psql -conn local match (n:User) return n limit 10 + +To also see the translator's internal SQL AST alongside the formatted +query: + + dawgrun > translate-psql -conn local -dump-pg-ast match (n:User) return n limit 10 + +### Ask Postgres to EXPLAIN a translated query + +Runs the translation, prepends `EXPLAIN`, and dispatches it over the +named connection: + + dawgrun > explain-psql local "match (n:User) where n.name = 'alice' return n" + +### Execute a Cypher query + +Default `table` output: + + dawgrun > query-cypher local match (n:User) return n.name, n.objectid limit 5 + +`json` output, useful for piping into other tooling: + + dawgrun > query-cypher -format json local match (n:User) return n.name, n.objectid limit 5 + +An empty result set renders as `(0 rows)` in table mode and `[]` in +JSON mode. + +### Runtime tracing + +Capture a Go runtime trace for a subsequent command or block of work. +The trace file defaults to `trace.out` in the current directory: + + dawgrun > runtime-trace start + dawgrun > query-cypher local match (n) return count(n) + dawgrun > runtime-trace stop + +Open the resulting trace with `go tool trace trace.out`. + +## History + +The REPL persists command history to +`$XDG_CONFIG_HOME/dawgrun/history.txt` (or the platform equivalent), +capped at 1000 lines, so recent commands are available via the +arrow keys across sessions. + +## Styling + +Syntax highlighting style defaults to `monokai`, but can be configured via +the `DAWGRUN_STYLE` environment variable. +Any styles in [Chroma](https://github.com/alecthomas/chroma/tree/master/styles) are available for use as a syntax highlighting style. diff --git a/tools/dawgrun/cmd/dawgrun/art.txt b/tools/dawgrun/cmd/dawgrun/art.txt new file mode 100644 index 0000000..4131ca9 --- /dev/null +++ b/tools/dawgrun/cmd/dawgrun/art.txt @@ -0,0 +1,5 @@ + .--~~,__ + :-....,-------`~~'._.' + `-,,, ,_ ;'~U' + _,-' ,'`-__; '--. + (_/'~~ ''''(; diff --git a/tools/dawgrun/cmd/dawgrun/banner.txt b/tools/dawgrun/cmd/dawgrun/banner.txt new file mode 100644 index 0000000..9a5fb3f --- /dev/null +++ b/tools/dawgrun/cmd/dawgrun/banner.txt @@ -0,0 +1,18 @@ + ,---. ,-.-. _,---. + _,..---._ .--.' \ ,-..-.-./ \==\ _.='.'-, \ +/==/, - \ \==\-/\ \ |, \=/\=|- |==|/==.'- / +|==| _ _\/==/-|_\ ||- |/ |/ , /==/==/ - .-' +|==| .=. |\==\, - \\, , _|==|==|_ /_,-. +|==|,| | -|/==/ - ,|| - - , |==|==| , \_.' ) +|==| '=' /==/- /\ - \\ , - /==/\==\- , ( +|==|-, _`/\==\ _.\=\.-'|- /\ /==/ /==/ _ , / +`-.`.____.' `--` `--` `--` `--`------' + .-._ + .-.,.---. .--.-. .-.-./==/ \ .-._ + /==/ ` \/==/ -|/=/ ||==|, \/ /, / + |==|-, .=., |==| ,||=| -||==|- \| | + |==| '=' /==|- | =/ ||==| , | -| + |==|- , .'|==|, \/ - ||==| - _ | + |==|_ . ,'.|==|- , /|==| /\ , | + /==/ /\ , )==/ , _ .' /==/, | |- | + `--`-`--`--'`--`..---' `--`./ `--` diff --git a/tools/dawgrun/cmd/dawgrun/main.go b/tools/dawgrun/cmd/dawgrun/main.go new file mode 100644 index 0000000..3c90140 --- /dev/null +++ b/tools/dawgrun/cmd/dawgrun/main.go @@ -0,0 +1,185 @@ +package main + +import ( + "context" + _ "embed" + "fmt" + "log/slog" + "os" + "path" + "runtime/trace" + "strings" + + "github.com/charmbracelet/lipgloss" + "github.com/google/shlex" + repl "github.com/openengineer/go-repl" + + "github.com/specterops/dawgs/tools/dawgrun/pkg/commands" +) + +var ( + //go:embed art.txt + art string + + //go:embed banner.txt + banner string + + bannerStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#87dc70")). + Background(lipgloss.Color("#533d25")) +) + +func main() { + fmt.Printf("\n%s\n%s", + art, + bannerStyle.Render(banner), + ) + fmt.Printf("\n\n\n") + fmt.Printf(" ::: DAWGRUN REPL ::: Type 'help' for more info\n") + + userConfigDir, err := os.UserConfigDir() + if err != nil { + panic(fmt.Errorf("could not get user config dir: %w", err)) + } + + // TODO: Someday, also load a config file from here for highlighting color scheme, colors enablement, etc + appConfigBaseDir := path.Join(userConfigDir, "dawgrun") + if err := os.MkdirAll(appConfigBaseDir, 0o750); err != nil { + panic(fmt.Errorf("could not create dawgrun config dir: %w", err)) + } + + handler := new(handler) + handler.cmdScope = commands.NewScope() + handler.r = repl.NewRepl(handler, &repl.Options{ + HistoryFilePath: path.Join(appConfigBaseDir, "history.txt"), + HistoryMaxLines: 1000, + StatusWidgets: &repl.StatusWidgetFns{ + Right: makeConnectionsStatusWidget(handler.cmdScope), + }, + }) + + if err := handler.r.Loop(); err != nil { + slog.Error("repl encountered error", slog.String("error", err.Error())) + } +} + +var ( + _ repl.Handler = (*handler)(nil) + _ repl.Completer = (*handler)(nil) +) + +type handler struct { + r *repl.Repl + cmdScope *commands.Scope +} + +func (h *handler) Prompt() string { + return "dawgrun > " +} + +func (h *handler) Tab(buffer string) string { + return "" +} + +func (h *handler) Complete(buffer string) repl.Completion { + prefix, ok := commandCompletionPrefix(buffer) + if !ok { + return repl.Completion{} + } + + prefix = strings.ToLower(prefix) + commandNames := commands.SortedCommandNames() + matches := make([]string, 0, len(commandNames)) + + for _, commandName := range commandNames { + if strings.HasPrefix(commandName, prefix) { + matches = append(matches, commandName) + } + } + + if len(matches) == 0 { + return repl.Completion{} + } + + if len(matches) == 1 { + match := matches[0] + if match == prefix { + return repl.Completion{Insert: " "} + } + + return repl.Completion{Insert: match[len(prefix):] + " "} + } + + return repl.Completion{ + Message: "Commands:", + Candidates: matches, + } +} + +func commandCompletionPrefix(buffer string) (string, bool) { + trimmed := strings.TrimLeft(buffer, " \t") + if trimmed == "" { + return "", true + } + + if strings.HasSuffix(trimmed, " ") || strings.HasSuffix(trimmed, "\t") { + return "", false + } + + fields := strings.Fields(trimmed) + if len(fields) != 1 { + return "", false + } + + return fields[0], true +} + +func (h *handler) Eval(line string) string { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + fields, err := shlex.Split(line) + if err != nil { + return fmt.Sprintf("Unparseable command: '%s': %s", line, err) + } + + if len(fields) == 0 { + return "Woof!" + } + + command := strings.ToLower(fields[0]) + rest := fields[1:] + if cmd, ok := commands.Registry()[command]; ok { + cmdCtx := commands.NewCommandContext(ctx, h.r, h.cmdScope) + defer trace.StartRegion(cmdCtx, fmt.Sprintf("command-%s", command)).End() + err := cmd.Fn(cmdCtx, rest) + // Reset command's associated flags after exec + defer func() { + if cmd.ClearFlagsFn != nil { + cmd.ClearFlagsFn() + } + }() + if err != nil { + return fmt.Sprintf("%s failed: %v", command, err) + } + + out := cmdCtx.OutputString() + if !strings.HasSuffix(out, "\n") { + return out + "\n" + } else { + return out + } + } + + return fmt.Sprintf("Unknown command %s; try `help`?", command) +} + +func makeConnectionsStatusWidget(cmdScope *commands.Scope) repl.StatusWidgetFn { + return func(r *repl.Repl) string { + if numConns := cmdScope.GetNumConnections(); numConns == 0 { + return "No connections" + } else { + return fmt.Sprintf("%d connection(s)", numConns) + } + } +} diff --git a/tools/dawgrun/docs/RFC.md b/tools/dawgrun/docs/RFC.md new file mode 100644 index 0000000..64f0374 --- /dev/null +++ b/tools/dawgrun/docs/RFC.md @@ -0,0 +1,219 @@ +--- +bh-rfc: unlisted +title: "dawgrun: Playground for DAWGS" +authors: | + [Sean Johnson](sjohnson@specterops.io) +status: DRAFT +created: 2026-01-29 +audiences: | + DAWGS maintainers and contributors + Engineers in adjacent repositories who author or debug DAWGS-backed queries +--- + +# dawgrun: Playground for DAWGS + +## 1. Overview + +`dawgrun` is a developer tool for exploring, debugging, and validating DAWGS behavior from a single interactive surface. + +Today, `dawgrun` operates as a REPL focused on PostgreSQL-backed DAWGS workflows. It supports opening DAWGS-compatible database connections, inspecting kind mappings, parsing CySQL/Cypher queries, translating those queries to PostgreSQL, running translated query plans with `EXPLAIN`, and executing Cypher directly. + +This RFC formalizes the current state of the tool and proposes an incremental roadmap that keeps `dawgrun` intentionally broad in mission: a long-lived playground for DAWGS debugging and experimentation. + +## 2. Motivation & Goals + +DAWGS development currently depends on a mix of tests, ad hoc scripts, and one-off debugging approaches. Those methods are useful but fragmented, and they do not provide a single place to inspect parser output, translation behavior, backend kind mapping, and live query execution together. + +`dawgrun` is meant to close that gap by providing a practical, team-owned workflow tool that can be used while authoring features, investigating regressions, and workshopping query behavior across repositories that depend on DAWGS. + +- **Unify Debugging Workflows** - Provide one tool that centralizes query parsing, translation inspection, kind resolution, and execution against live backends. +- **Shorten Iteration Loops** - Reduce friction between changing DAWGS code and validating behavior against real-world query/data scenarios. +- **Improve Translation Visibility** - Make AST and generated SQL output easy to inspect, compare, and discuss during development. +- **Support Real Backends** - Enable debugging against actual DAWGS-compatible data sources rather than only synthetic unit-test fixtures. +- **Remain Extensible** - Preserve room for deeper introspection capabilities as DAWGS evolves. + +## 3. Considerations + +### 3.1 Impact on Existing Systems + +`dawgrun` is an additive developer tool and does not change runtime behavior of DAWGS libraries in production paths. + +The tool integrates at DAWGS API boundaries and should continue to do so. New features SHOULD prefer composition over invasive changes to core DAWGS packages unless a clear library-level improvement is justified independently. + +### 3.2 Security & Compliance + +`dawgrun` targets engineering and test workflows, but it still operates on live data connections. Usage SHOULD assume standard safeguards for credentials, access controls, and environment boundaries. + +At minimum: + +- Connection strings MUST be handled as sensitive values. +- Query output MAY include sensitive graph attributes and SHOULD be used accordingly. +- Features that increase automation or scripting SHOULD avoid encouraging unsafe credential handling patterns. + +### 3.3 Drawbacks & Alternatives + +`dawgrun` currently uses a modified fork of `github.com/openengineer/go-repl` and carries that maintenance burden in-tree. This provides needed control, but it also means REPL behavior is partly owned by this project. + +Primary trade-offs: + +- A REPL-first interface is efficient for iterative debugging, but less direct for batch automation. +- A broad mission can drift into unstructured feature sprawl without explicit scope guardrails. +- Maintaining tooling inside this repository creates ongoing upkeep cost. + +Alternatives include continuing with ad hoc scripts or embedding isolated diagnostics in each dependent repository. Those options lower central tool complexity, but they duplicate effort and weaken shared debugging conventions. + +### 3.4 Audience + +The primary audience is engineers actively developing DAWGS. + +Secondary audiences include engineers in BHCE/BHE and other adjacent codebases who need to workshop or debug DAWGS-backed query behavior before committing changes to application-level implementations. + +### 3.5 Adoption Model + +`dawgrun` is an as-needed engineering tool, not a formal user-facing product feature. + +Adoption is intentionally lightweight: + +- No coordinated organization-wide rollout is required. +- Capabilities are expected to evolve incrementally as development needs arise. +- Changes SHOULD still be documented clearly so workflows remain discoverable for current and future contributors. + +## 4. Current State + +This section documents the implemented baseline at the time of writing. + +### 4.1 Runtime Model + +`dawgrun` currently runs as an interactive REPL with command parsing, persistent session history, and command-name completion. + +- Prompt: `dawgrun >` +- History file: `$XDG_CONFIG_HOME/dawgrun/history.txt` (or platform equivalent) +- Status widget: active connection count +- Completion: command-name matching with candidate popover + +### 4.2 Implemented Command Surface + +The current command set includes: + +- `open-pg-db` - open a named DAWGS-compatible PostgreSQL connection +- `load-db-kinds` - refresh and print kind mappings for a connection +- `lookup-kind` / `lookup-kind-id` - resolve kind names and IDs +- `parse` - parse CySQL/Cypher into AST and dump highlighted output +- `translate-psql` - translate CySQL/Cypher to PostgreSQL SQL +- `explain-psql` - translate, prepend `EXPLAIN`, and execute against PostgreSQL +- `query-cypher` - execute query over active connection with table/JSON output +- `runtime-trace` - start/stop Go runtime tracing to a file +- `help`, `exit`, `quit` + +### 4.3 Query and Translation Workflows + +Current query workflows support: + +- Parsing query input into Cypher AST structures for inspection. +- Translating Cypher into PostgreSQL statements. +- Optional dump of translator SQL AST (`translate-psql -dump-pg-ast`). +- Optional kind-aware translation against a named connection (`translate-psql -conn `). +- `EXPLAIN` execution against translated SQL for planner visibility. + +### 4.4 Data and Kind Inspection + +Named connections and lazily loaded kind maps provide the basis for cross-command introspection. + +- Kind maps are fetched from the live backend and cached in command scope. +- Name/ID lookup helpers support debugging mismatches in query translation and execution. +- Current backend connection command is PostgreSQL-specific. + +### 4.5 Known Constraints + +- Backend connection management is PostgreSQL-specific (`open-pg-db`); broader driver support is not yet implemented. +- REPL input uses shell-style tokenization, which affects quoting behavior for Cypher string literals. +- The tool is interactive-first; scriptability exists only in limited form through command composition and shell invocation patterns. + +## 5. Details of the Proposal + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt). + +This proposal keeps the broad long-term goal intact while defining concrete near-term improvements. + +### 5.1 Data Inspection + +`dawgrun` MUST continue to support interactive inspection against live graph data and kind metadata. + +Requirements: + +- The tool MUST preserve straightforward connection management for at least PostgreSQL. +- The tool SHOULD provide a path to generic `open-db` behavior for additional DAWGS drivers. +- Kind inspection SHOULD remain available as first-class commands and SHOULD support both name-to-ID and ID-to-name workflows. +- Refresh semantics for kind mappings MUST remain explicit and predictable. + +Implementation direction: + +- Evolve command surface from backend-specific naming toward backend-agnostic patterns where practical. +- Maintain compatibility with existing `open-pg-db` usage during migration. + +### 5.2 Query Tooling + +`dawgrun` MUST provide robust tools for understanding query lifecycle transformations. + +Requirements: + +- Users MUST be able to parse and inspect CySQL/Cypher AST output. +- Users MUST be able to translate CySQL/Cypher into PostgreSQL queries with and without live kind mapping. +- Users SHOULD be able to inspect both final SQL and intermediate translator AST artifacts. +- Users SHOULD be able to validate planner behavior through `EXPLAIN` on translated output. +- Query execution output MUST remain consumable both by humans (table mode) and tooling (JSON mode). + +Implementation direction: + +- Preserve current `parse`, `translate-psql`, `explain-psql`, and `query-cypher` workflows as the stable baseline. +- Add workflow shortcuts only when they map to repeatable debugging use-cases and do not obscure underlying behavior. + +### 5.3 Developer Tooling + +`dawgrun` SHOULD continue evolving into a generalized DAWGS debugging workbench. + +Target capabilities: + +- Support manually stepping query translation paths for deeper debugging visibility. +- Support interactive construction and execution of DAWGS graph-query components. +- Support generation helpers for translation test-case authoring. +- Improve automation friendliness for repeated local workflows (for example, repeatable startup state and command scripts). + +Guardrails for expansion: + +- New capabilities MUST correspond to concrete DAWGS engineering workflows. +- REPL ergonomics and clarity SHOULD be favored over highly specialized one-off behavior. +- Additions SHOULD document whether they are implemented, experimental, or roadmap-only. + +## 6. Scope Boundaries + +`dawgrun` is intentionally broad in mission, but not unbounded in purpose. + +In scope: + +- Any developer-facing capability that materially improves DAWGS debugging, query workshopping, or translation validation. +- Features that help engineers observe behavior across parser, translator, kind mapping, and backend execution layers. + +Out of scope: + +- End-user product UX concerns unrelated to DAWGS engineering workflows. +- Replacing formal test suites, CI validation, or benchmark infrastructure. +- Adding features that do not map to recurring debugging or development needs. + +## 7. Success Criteria + +This RFC is successful when `dawgrun` is treated as the default developer entry point for interactive DAWGS debugging by its primary audience. + +Practical indicators include: + +- Engineers can move from query idea to parser/translator/execute feedback in a single session. +- Kind mapping and translation issues can be reproduced and inspected without custom one-off scripts. +- New DAWGS contributors can discover and use common debugging workflows through command help and documentation. +- Expansion work remains coherent with the tool's mission rather than fragmenting into unrelated utilities. + +## 8. Open Questions + +- What is the preferred migration path from `open-pg-db` to a generic multi-driver connection interface? +- Which automation model is most valuable first: startup config profiles, scripted command batches, or both? +- What level of translation-step introspection is useful by default versus too noisy for routine workflows? +- Should roadmap features be tagged by maturity level directly in help output to clarify supported versus experimental behavior? diff --git a/tools/dawgrun/go.mod b/tools/dawgrun/go.mod new file mode 100644 index 0000000..673c3eb --- /dev/null +++ b/tools/dawgrun/go.mod @@ -0,0 +1,68 @@ +module github.com/specterops/dawgs/tools/dawgrun + +go 1.25.0 + +require ( + github.com/alecthomas/chroma/v2 v2.13.0 + github.com/charmbracelet/lipgloss v1.1.0 + github.com/davecgh/go-spew v1.1.1 + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 + github.com/jedib0t/go-pretty/v6 v6.7.8 + github.com/kanmu/go-sqlfmt v0.0.2-0.20200215095417-d1e63e2ee5eb + github.com/mitchellh/go-wordwrap v1.0.1 + github.com/openengineer/go-repl v0.0.0-00010101000000-000000000000 + github.com/specterops/dawgs v0.4.18 + golang.org/x/term v0.39.0 +) + +require ( + github.com/RoaringBitmap/roaring/v2 v2.16.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.39.3 // indirect + github.com/aws/aws-sdk-go-v2/config v1.31.13 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.18.17 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.10 // indirect + github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.10 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.10 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.10 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.10 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.29.7 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.38.7 // indirect + github.com/aws/smithy-go v1.23.1 // indirect + github.com/axiomhq/hyperloglog v0.2.6 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/bits-and-blooms/bitset v1.24.4 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/gammazero/deque v1.2.1 // indirect + github.com/jackc/pgio v1.0.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgtype v1.14.4 // indirect + github.com/jackc/pgx/v5 v5.9.2 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/kamstrup/intmap v0.5.2 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mschoch/smat v0.2.0 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/neo4j/neo4j-go-driver/v5 v5.28.4 // indirect + github.com/pkg/errors v0.8.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.35.0 // indirect +) + +replace github.com/openengineer/go-repl => ./pkg/go-repl diff --git a/tools/dawgrun/go.sum b/tools/dawgrun/go.sum new file mode 100644 index 0000000..c5ec45c --- /dev/null +++ b/tools/dawgrun/go.sum @@ -0,0 +1,323 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/RoaringBitmap/roaring/v2 v2.16.0 h1:Kys1UNf49d5W8Tq3bpuAhIr/Z8/yPB+59CO8A6c/BbE= +github.com/RoaringBitmap/roaring/v2 v2.16.0/go.mod h1:eq4wdNXxtJIS/oikeCzdX1rBzek7ANzbth041hrU8Q4= +github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= +github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.13.0 h1:VP72+99Fb2zEcYM0MeaWJmV+xQvz5v5cxRHd+ooU1lI= +github.com/alecthomas/chroma/v2 v2.13.0/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/aws/aws-sdk-go-v2 v1.39.3 h1:h7xSsanJ4EQJXG5iuW4UqgP7qBopLpj84mpkNx3wPjM= +github.com/aws/aws-sdk-go-v2 v1.39.3/go.mod h1:yWSxrnioGUZ4WVv9TgMrNUeLV3PFESn/v+6T/Su8gnM= +github.com/aws/aws-sdk-go-v2/config v1.31.13 h1:wcqQB3B0PgRPUF5ZE/QL1JVOyB0mbPevHFoAMpemR9k= +github.com/aws/aws-sdk-go-v2/config v1.31.13/go.mod h1:ySB5D5ybwqGbT6c3GszZ+u+3KvrlYCUQNo62+hkKOFk= +github.com/aws/aws-sdk-go-v2/credentials v1.18.17 h1:skpEwzN/+H8cdrrtT8y+rvWJGiWWv0DeNAe+4VTf+Vs= +github.com/aws/aws-sdk-go-v2/credentials v1.18.17/go.mod h1:Ed+nXsaYa5uBINovJhcAWkALvXw2ZLk36opcuiSZfJM= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.10 h1:UuGVOX48oP4vgQ36oiKmW9RuSeT8jlgQgBFQD+HUiHY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.10/go.mod h1:vM/Ini41PzvudT4YkQyE/+WiQJiQ6jzeDyU8pQKwCac= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.10 h1:xfgjONWMae6+y//dlhVukwt9N+I++FPuiwcQt7DI7Qg= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.10/go.mod h1:FO6aarJTHA2N3S8F2A4wKfnX9Jr6MPerJFaqoLgTctU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.10 h1:mj/bdWleWEh81DtpdHKkw41IrS+r3uw1J/VQtbwYYp8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.10/go.mod h1:7+oEMxAZWP8gZCyjcm9VicI0M61Sx4DJtcGfKYv2yKQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.10 h1:wh+/mn57yhUrFtLIxyFPh2RgxgQz/u+Yrf7hiHGHqKY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.10/go.mod h1:7zirD+ryp5gitJJ2m1BBux56ai8RIRDykXZrJSp540w= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2 h1:xtuxji5CS0JknaXoACOunXOYOQzgfTvGAc9s2QdCJA4= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2/go.mod h1:zxwi0DIR0rcRcgdbl7E2MSOvxDyyXGBlScvBkARFaLQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.10 h1:DRND0dkCKtJzCj4Xl4OpVbXZgfttY5q712H9Zj7qc/0= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.10/go.mod h1:tGGNmJKOTernmR2+VJ0fCzQRurcPZj9ut60Zu5Fi6us= +github.com/aws/aws-sdk-go-v2/service/sso v1.29.7 h1:fspVFg6qMx0svs40YgRmE7LZXh9VRZvTT35PfdQR6FM= +github.com/aws/aws-sdk-go-v2/service/sso v1.29.7/go.mod h1:BQTKL3uMECaLaUV3Zc2L4Qybv8C6BIXjuu1dOPyxTQs= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.2 h1:scVnW+NLXasGOhy7HhkdT9AGb6kjgW7fJ5xYkUaqHs0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.2/go.mod h1:FRNCY3zTEWZXBKm2h5UBUPvCVDOecTad9KhynDyGBc0= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.7 h1:VEO5dqFkMsl8QZ2yHsFDJAIZLAkEbaYDB+xdKi0Feic= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.7/go.mod h1:L1xxV3zAdB+qVrVW/pBIrIAnHFWHo6FBbFe4xOGsG/o= +github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M= +github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/axiomhq/hyperloglog v0.2.6 h1:sRhvvF3RIXWQgAXaTphLp4yJiX4S0IN3MWTaAgZoRJw= +github.com/axiomhq/hyperloglog v0.2.6/go.mod h1:YjX/dQqCR/7QYX0g8mu8UZAjpIenz1FKM71UEsjFoTo= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= +github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mTEIGbvhcYU3S8+uSNkuMjx/qZFfhtM= +github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/gammazero/deque v1.2.1 h1:9fnQVFCCZ9/NOc7ccTNqzoKd1tCWOqeI05/lPqFPMGQ= +github.com/gammazero/deque v1.2.1/go.mod h1:5nSFkzVm+afG9+gy0VIowlqVAW4N8zNcMne+CMQVD2g= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= +github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= +github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgtype v1.14.4 h1:fKuNiCumbKTAIxQwXfB/nsrnkEI6bPJrrSiMKgbJ2j8= +github.com/jackc/pgtype v1.14.4/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU= +github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jedib0t/go-pretty/v6 v6.7.8 h1:BVYrDy5DPBA3Qn9ICT+PokP9cvCv1KaHv2i+Hc8sr5o= +github.com/jedib0t/go-pretty/v6 v6.7.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/kamstrup/intmap v0.5.2 h1:qnwBm1mh4XAnW9W9Ue9tZtTff8pS6+s6iKF6JRIV2Dk= +github.com/kamstrup/intmap v0.5.2/go.mod h1:gWUVWHKzWj8xpJVFf5GC0O26bWmv3GqdnIX/LMT6Aq4= +github.com/kanmu/go-sqlfmt v0.0.2-0.20200215095417-d1e63e2ee5eb h1:Ztn62UtaXoFlHJIrH0AuNQrMhE355paIqZn3ik6bHNk= +github.com/kanmu/go-sqlfmt v0.0.2-0.20200215095417-d1e63e2ee5eb/go.mod h1:1+jKeqi65LLLs1GSNFRn4G/dkgg3TWeT6DTZLLQP2eM= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= +github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/neo4j/neo4j-go-driver/v5 v5.28.4 h1:7toxehVcYkZbyxV4W3Ib9VcnyRBQPucF+VwNNmtSXi4= +github.com/neo4j/neo4j-go-driver/v5 v5.28.4/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k= +github.com/openengineer/go-terminal v0.0.0-20220304032943-93486212aca4/go.mod h1:Dx5mNI0A2naWQySM7zXOl/NT5QWs2sfvcQxq1tCbQVY= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/specterops/dawgs v0.4.18 h1:fjRefdrLGMSI3lScTL+OYHuKK6/W8ssiVVL5gS5xBgc= +github.com/specterops/dawgs v0.4.18/go.mod h1:3ImG8huVqdRQECrupeJQH0ysG7lGTBR9lDsromYzcaU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/tools/dawgrun/justfile b/tools/dawgrun/justfile new file mode 100644 index 0000000..1582e2b --- /dev/null +++ b/tools/dawgrun/justfile @@ -0,0 +1,36 @@ +set dotenv-load := true +set dotenv-filename := ".build.env" + +default: build + +vet: + go mod tidy + go fmt ./... + go vet ./... + +build *BUILDARGS: vet + go build -o dawgrun {{ BUILDARGS }} ./cmd/... + +build-with-dawgs path_to_dawgs *BUILDARGS: + echo 'GOWORK=off' > .build.env + go mod edit -replace=github.com/specterops/dawgs={{ path_to_dawgs }} + just build {{ BUILDARGS }} + +build-with-upstream *BUILDARGS: + #!/usr/bin/env sh + [ -f .build.env ] && { + rm -f .build.env + unset GOWORK + just build-with-upstream {{ BUILDARGS }} + } || { + go work edit -dropreplace=github.com/specterops/dawgs + go mod edit -dropreplace=github.com/specterops/dawgs + just build {{ BUILDARGS }} + } + +clean: + @rm -f dawgrun + +[arg("port", long="port")] +debug port="38697": build + dlv exec --continue --accept-multiclient --headless --listen 127.0.0.1:{{ port }} ./dawgrun diff --git a/tools/dawgrun/pkg/commands/cypher.go b/tools/dawgrun/pkg/commands/cypher.go new file mode 100644 index 0000000..0560f97 --- /dev/null +++ b/tools/dawgrun/pkg/commands/cypher.go @@ -0,0 +1,490 @@ +package commands + +import ( + "encoding/json" + "flag" + "fmt" + "os" + "strings" + + "github.com/davecgh/go-spew/spew" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/jedib0t/go-pretty/v6/text" + "github.com/kanmu/go-sqlfmt/sqlfmt" + "github.com/specterops/dawgs/cypher/models/pgsql/format" + "github.com/specterops/dawgs/cypher/models/pgsql/translate" + "github.com/specterops/dawgs/graph" + "golang.org/x/term" + + "github.com/specterops/dawgs/tools/dawgrun/pkg/stubs" +) + +const ( + queryCypherOutputFormatTable = "table" + queryCypherOutputFormatJSON = "json" +) + +func parseCmd() CommandDesc { + return CommandDesc{ + args: []string{"<...query>"}, + help: "Parses and dumps a Cypher query to AST form.", + + Fn: func(ctx *CommandContext, fields []string) error { + query, err := parseQueryArray(fields) + if err != nil { + return fmt.Errorf("error trying to parse query '%s': %w", fields, err) + } + + ctx.output.WriteHighlighted(spew.Sdump(query), "golang") + return nil + }, + } +} + +func translateToPsqlCmd() CommandDesc { + flagSet := flag.NewFlagSet("translate-psql", flag.ContinueOnError) + + var ( + kindMapperConnRef = "" + dumpTranslatedAst = false + ) + + flagSet.StringVar(&kindMapperConnRef, "conn", "", "Connection reference for choosing a kind mapper") + flagSet.BoolVar(&dumpTranslatedAst, "dump-pg-ast", false, "Whether to dump the translator's constructed AST") + + return CommandDesc{ + args: []string{"[flags]", "<...query>"}, + help: "Parses a query and converts it to the underlying PostgreSQL query", + desc: "Does a bunch of magic to fully translate a Cypher query into a PostgreSQL query", + flags: flagSet, + + ClearFlagsFn: func() { + kindMapperConnRef = "" + dumpTranslatedAst = false + }, + Fn: func(ctx *CommandContext, fields []string) error { + if err := flagSet.Parse(fields); err != nil { + return fmt.Errorf("could not parse flags: %w", err) + } + + fields = flagSet.Args() + query, err := parseQueryArray(fields) + if err != nil { + return fmt.Errorf("error trying to parse query '%s': %w", fields, err) + } + + kindMapper := stubs.EmptyMapper() + if kindMapperConnRef != "" { + // Fetch kinds regardless of if it's already loaded. + kindMap, err := loadKindMap(ctx, kindMapperConnRef) + if err != nil { + return fmt.Errorf("could not load kind map for explain: %w", err) + } + kindMapper = stubs.MapperFromKindMap(kindMap) + } + + result, err := translate.Translate(ctx, query, kindMapper, nil) + if err != nil { + return fmt.Errorf("could not translate cypher query to pgsql: %w", err) + } + if dumpTranslatedAst { + fmt.Fprintf(ctx.output, "TRANSLATOR AST\n\n") + ctx.output.WriteHighlighted(spew.Sdump(result.Statement), "golang") + fmt.Fprintf(ctx.output, "\n") + } + + // Certain queries will materialize parameters into the output when translated, so we need to build + // an OutputBuilder so we can carry forward those params. + queryBuilder := format.NewOutputBuilder() + if result.Parameters != nil { + queryBuilder.WithMaterializedParameters(result.Parameters) + } + + sqlQuery, err := format.Statement(result.Statement, queryBuilder) + if err != nil { + return fmt.Errorf("could not format translated statement into a string query: %w", err) + } + + formattedQuery, err := sqlfmt.Format(sqlQuery, &sqlfmt.Options{ + Distance: 0, + }) + if err != nil { + ctx.output.Warnf("could not format query: %s", err.Error()) + formattedQuery = sqlQuery + } + + ctx.output.WriteHighlighted(formattedQuery, "postgres") + return nil + }, + } +} + +func explainAsPsqlCmd() CommandDesc { + return CommandDesc{ + args: []string{"", "<...query>"}, + help: "Explains a translated query over an active PG connection", + desc: "Asks the PG query planner to explain the (translated) Cypher query in PG terms", + + Fn: func(ctx *CommandContext, fields []string) error { + if len(fields) < 2 { + return fmt.Errorf("invalid usage, requires: ") + } + + connName := fields[0] + conn, ok := ctx.scope.connections[connName] + if !ok { + return fmt.Errorf("connection %s not found; did you `open` it?", connName) + } + + // Fetch kinds regardless of if it's already loaded. + kindMap, err := loadKindMap(ctx, connName) + if err != nil { + return fmt.Errorf("could not load kind map for explain: %w", err) + } + + query, err := parseQueryArray(fields[1:]) + if err != nil { + return fmt.Errorf("could not parse query: %w", err) + } + + // Populate a DumbKindMapper from the database's kinds table + kindMapper := stubs.MapperFromKindMap(kindMap) + result, err := translate.Translate(ctx, query, kindMapper, nil) + if err != nil { + return fmt.Errorf("could not translate cypher query to pgsql: %w", err) + } + + // Certain queries will materialize parameters into the output when translated, so we need to build + // an OutputBuilder so we can carry forward those params. + queryBuilder := format.NewOutputBuilder() + if result.Parameters != nil { + queryBuilder.WithMaterializedParameters(result.Parameters) + } + + sqlQuery, err := format.Statement(result.Statement, queryBuilder) + if err != nil { + return fmt.Errorf("could not format translated statement into a string query: %w", err) + } + + formattedQuery, err := sqlfmt.Format(sqlQuery, &sqlfmt.Options{ + Distance: 2, + }) + if err != nil { + ctx.output.Warnf("could not format query: %s", err.Error()) + formattedQuery = sqlQuery + } + explainSQLQuery := fmt.Sprintf("EXPLAIN %s", formattedQuery) + ctx.output.WriteHighlighted(explainSQLQuery, "postgres") + fmt.Fprint(ctx.output, "\n\n") + + err = conn.ReadTransaction(ctx, func(tx graph.Transaction) error { + result := tx.Raw(explainSQLQuery, nil) + if err := result.Error(); err != nil { + return fmt.Errorf("error running raw query: '%s': %w", explainSQLQuery, err) + } + defer result.Close() + + var value string + for result.Next() { + if err := graph.ScanNextResult(result, &value); err != nil { + return fmt.Errorf("could not scan EXPLAIN row: %w", err) + } + fmt.Fprintf(ctx.output, " %s\n", value) + } + + return nil + }) + if err != nil { + return fmt.Errorf("could not run EXPLAIN query: %w", err) + } + + return nil + }, + } +} + +func queryCypherCmd() CommandDesc { + flagSet := flag.NewFlagSet("query-cypher", flag.ContinueOnError) + + outputFormat := queryCypherOutputFormatTable + flagSet.StringVar(&outputFormat, "format", queryCypherOutputFormatTable, "Output format: table or json") + + return CommandDesc{ + args: []string{"[flags]", "", "<...query>"}, + help: "Executes a Cypher query and renders table or JSON output", + desc: "Runs a Cypher query over an active backend connection and prints fetched rows", + flags: flagSet, + + ClearFlagsFn: func() { + outputFormat = queryCypherOutputFormatTable + }, + Fn: func(ctx *CommandContext, fields []string) error { + outputFormat = queryCypherOutputFormatTable + if err := flagSet.Parse(fields); err != nil { + return fmt.Errorf("could not parse flags: %w", err) + } + + fields = flagSet.Args() + if len(fields) < 2 { + return fmt.Errorf("invalid usage, requires: ") + } + + outputFormat = strings.ToLower(strings.TrimSpace(outputFormat)) + switch outputFormat { + case queryCypherOutputFormatTable, queryCypherOutputFormatJSON: + default: + return fmt.Errorf("invalid output format %q; expected one of: %s, %s", outputFormat, queryCypherOutputFormatTable, queryCypherOutputFormatJSON) + } + + connName := fields[0] + conn, ok := ctx.scope.connections[connName] + if !ok { + return fmt.Errorf("connection %s not found; did you `open` it?", connName) + } + + cypherQuery := strings.Join(fields[1:], " ") + + return conn.ReadTransaction(ctx, func(tx graph.Transaction) error { + result := tx.Query(cypherQuery, nil) + if err := result.Error(); err != nil { + return fmt.Errorf("error running cypher query '%s': %w", cypherQuery, err) + } + defer result.Close() + + switch outputFormat { + case queryCypherOutputFormatJSON: + return queryCypherOutputJSON(ctx, result) + case queryCypherOutputFormatTable: + return queryCypherOutputTable(ctx, result) + } + + return fmt.Errorf("unknown output format: %s", outputFormat) + }) + }, + } +} + +func queryCypherOutputJSON(ctx *CommandContext, result graph.Result) error { + var outputColumns []string + + fmt.Fprint(ctx.output, "[\n") + + rowCount := 0 + for result.Next() { + values := result.Values() + insertFormat := ",\n %s" + + if rowCount == 0 { + outputColumns = buildCypherResultColumns(result.Keys(), len(values)) + insertFormat = " %s" + } + + rowOutput, err := json.MarshalIndent(buildCypherResultJSONRow(outputColumns, values), " ", " ") + if err != nil { + return fmt.Errorf("error marshalling JSON row: %v: %w", values, err) + } + + fmt.Fprintf(ctx.output, insertFormat, rowOutput) + rowCount++ + } + + if err := result.Error(); err != nil { + return fmt.Errorf("error fetching query rows: %w", err) + } + + fmt.Fprint(ctx.output, "\n]\n") + + return nil +} + +func queryCypherOutputTable(ctx *CommandContext, result graph.Result) error { + var ( + outputColumns []string + outputTable table.Writer + ) + + outputTable = table.NewWriter() + style := table.StyleRounded + style.Options.SeparateRows = true + style.Size.WidthMax = cypherResultTableWidth() + outputTable.SetStyle(style) + + rowCount := 0 + for result.Next() { + values := result.Values() + + if rowCount == 0 { + outputColumns = buildCypherResultColumns(result.Keys(), len(values)) + + outputTable.AppendHeader(buildCypherResultHeader(outputColumns)) + outputTable.SetColumnConfigs(buildCypherResultColumnConfigs(len(outputColumns), cypherResultTableWidth())) + } + + outputTable.AppendRow(buildCypherResultRow(values)) + rowCount++ + } + + if err := result.Error(); err != nil { + return fmt.Errorf("error fetching query rows: %w", err) + } + + if rowCount == 0 { + fmt.Fprint(ctx.output, "(0 rows)\n") + return nil + } + + fmt.Fprint(ctx.output, outputTable.Render()) + fmt.Fprintf(ctx.output, "\n(%d rows)\n", rowCount) + + return nil +} + +func cypherResultTableWidth() int { + const ( + fallbackWidth = 120 + ) + + if width, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil { + return width - 2 + } + + return fallbackWidth +} + +func buildCypherResultColumnConfigs(columnCount, tableWidth int) []table.ColumnConfig { + if columnCount == 0 { + return nil + } + + const ( + minColumnWidth = 12 + innerPadding = 3 + ) + + availableWidth := tableWidth - 1 - (columnCount * innerPadding) + columnWidth := availableWidth / columnCount + if columnWidth < minColumnWidth { + columnWidth = minColumnWidth + } + + configs := make([]table.ColumnConfig, 0, columnCount) + for idx := 0; idx < columnCount; idx++ { + configs = append(configs, table.ColumnConfig{ + Number: idx + 1, + WidthMax: columnWidth, + WidthMaxEnforcer: text.WrapHard, + }) + } + + return configs +} + +func buildCypherResultColumns(keys []string, numValues int) []string { + columns := append([]string{}, keys...) + + if len(columns) < numValues { + for idx := len(columns); idx < numValues; idx++ { + columns = append(columns, fmt.Sprintf("column_%d", idx+1)) + } + } + + return columns +} + +func buildCypherResultHeader(columns []string) table.Row { + row := make(table.Row, len(columns)) + for idx, key := range columns { + row[idx] = key + } + + return row +} + +func buildCypherResultJSONRow(columns []string, values []any) map[string]any { + row := make(map[string]any, len(columns)) + for idx, key := range columns { + if idx < len(values) { + row[key] = formatCypherResultJSONValue(values[idx]) + } else { + row[key] = nil + } + } + + return row +} + +func formatCypherResultJSONValue(value any) any { + switch typed := value.(type) { + case nil: + return nil + case bool, + int, + int8, + int16, + int32, + int64, + uint, + uint8, + uint16, + uint32, + uint64, + float32, + float64, + string: + return typed + case graph.ID: + return typed.Uint64() + case []byte: + return string(typed) + default: + if marshaled, err := json.Marshal(typed); err == nil { + var normalized any + if err := json.Unmarshal(marshaled, &normalized); err == nil { + return normalized + } + } + + return fmt.Sprintf("%v", typed) + } +} + +func buildCypherResultRow(values []any) table.Row { + row := make(table.Row, len(values)) + for idx, value := range values { + row[idx] = formatCypherResultValue(value) + } + + return row +} + +func formatCypherResultValue(value any) any { + switch typed := value.(type) { + case nil: + return "" + case bool, + int, + int8, + int16, + int32, + int64, + uint, + uint8, + uint16, + uint32, + uint64, + float32, + float64, + string: + return typed + case []byte: + return string(typed) + case fmt.Stringer: + return typed.String() + default: + if marshaled, err := json.Marshal(typed); err == nil { + return string(marshaled) + } + + return fmt.Sprintf("%v", typed) + } +} diff --git a/tools/dawgrun/pkg/commands/db.go b/tools/dawgrun/pkg/commands/db.go new file mode 100644 index 0000000..3119860 --- /dev/null +++ b/tools/dawgrun/pkg/commands/db.go @@ -0,0 +1,199 @@ +package commands + +import ( + "fmt" + "strconv" + + "github.com/davecgh/go-spew/spew" + "github.com/specterops/dawgs" + "github.com/specterops/dawgs/drivers" + "github.com/specterops/dawgs/drivers/pg" + "github.com/specterops/dawgs/graph" + + "github.com/specterops/dawgs/tools/dawgrun/pkg/stubs" +) + +// TODO(seanj): Convert to generic open-db command that supports any available driver +func openPGDBCmd() CommandDesc { + return CommandDesc{ + args: []string{"", ""}, + help: "Connects to a specified DAWGS-compatible Postgres DB to do graph introspection.", + + Fn: func(ctx *CommandContext, fields []string) error { + if len(fields) < 2 { + return fmt.Errorf("invalid usage: open-pg-db ") + } + + name := fields[0] + connStr := fields[1] + connPool, err := pg.NewPool(drivers.DatabaseConfiguration{ + Connection: connStr, + }) + if err != nil { + return fmt.Errorf("error opening connection pool: %w", err) + } + + querier, err := dawgs.Open(ctx, "pg", dawgs.Config{ + ConnectionString: connStr, + Pool: connPool, + }) + if err != nil { + return fmt.Errorf("error opening database connection '%s': %w", connStr, err) + } + + if existingConn, ok := ctx.scope.connections[name]; ok { + // Warn+close existing connection before overwriting it + ctx.output.Warnf("Discarding previous connection for '%s'", name) + if err := existingConn.Close(ctx); err != nil { + return fmt.Errorf("could not close previous connection '%s' for overwriting: %w", name, err) + } + + // Wipe out handles and resources for this connection + ctx.scope.DropConnection(name) + } + + fmt.Fprintf(ctx.output, "Opened connection '%s'\n", name) + ctx.scope.AddConnection(name, querier) + + return nil + }, + } +} + +func getPGDBKinds() CommandDesc { + return CommandDesc{ + args: []string{""}, + help: "Loads/shows the kind mapping from the specified DB into the 'active set'", + + Fn: func(ctx *CommandContext, fields []string) error { + if len(fields) != 1 { + return fmt.Errorf("invalid usage: load-db-kinds ") + } + + connName := fields[0] + if kindMap, err := loadKindMap(ctx, connName); err != nil { + return fmt.Errorf("could not load kind map: %w", err) + } else { + ctx.scope.connKindMaps[connName] = kindMap + fmt.Fprintf(ctx.output, "Loaded kind map from connection '%s':\n", connName) + ctx.output.WriteHighlighted(spew.Sdump(kindMap), "golang") + } + + return nil + }, + } +} + +func loadKindMap(ctx *CommandContext, connName string) (stubs.KindMap, error) { + conn, ok := ctx.scope.connections[connName] + if !ok { + return nil, fmt.Errorf("unknown connection %s; did you `open` it?", connName) + } + + // Force a refresh from the database backend + if err := conn.RefreshKinds(ctx); err != nil { + return nil, fmt.Errorf("could not refresh kinds for connection %s: %w", connName, err) + } + + // Load kinds list + kinds, err := conn.FetchKinds(ctx) + if err != nil { + return nil, fmt.Errorf("could not fetch kinds for connection %s: %w", connName, err) + } + + // Coerce a pg.Driver out of the conn + driver, ok := conn.(*pg.Driver) + if !ok { + return nil, fmt.Errorf("connection %s is not a 'pg' connection", connName) + } + + // Map the kinds to their IDs + kindIds, err := driver.MapKinds(ctx, kinds) + if err != nil { + return nil, fmt.Errorf("could not map kinds to IDs: %s", err) + } + + kindMap := make(stubs.KindMap) + for idx, kind := range kinds { + kindMap[kindIds[idx]] = kind + } + + ctx.scope.connKindMaps[connName] = kindMap + + return kindMap, nil +} + +func lookupKindCmd() CommandDesc { + return CommandDesc{ + args: []string{"", ""}, + help: "Looks up a kind from database based on kind name", + + Fn: func(ctx *CommandContext, fields []string) error { + if len(fields) < 2 { + return fmt.Errorf("invalid usage: lookup-kind ") + } + + connName := fields[0] + kindName := fields[1] + + kindMap, ok := ctx.scope.connKindMaps[connName] + if !ok { + // Try to fetch the kind map if the connection is open + var err error + kindMap, err = loadKindMap(ctx, connName) + if err != nil { + return fmt.Errorf("could not fetch kind map: %w", err) + } + } + + mapper := stubs.MapperFromKindMap(kindMap) + if kindID, err := mapper.GetIDByKind(graph.StringKind(kindName)); err != nil { + return fmt.Errorf("could not look up kind: %w", err) + } else { + fmt.Fprintf(ctx.output, "Kind %s => %d", kindName, kindID) + } + + return nil + }, + } +} + +func lookupKindIDCmd() CommandDesc { + return CommandDesc{ + args: []string{"", ""}, + help: "Looks up a kind from database based on kind ID", + + Fn: func(ctx *CommandContext, fields []string) error { + if len(fields) < 2 { + return fmt.Errorf("invalid usage: lookup-kind-id ") + } + + connName := fields[0] + kindIDStr := fields[1] + + kindID, err := strconv.ParseInt(kindIDStr, 10, 16) + if err != nil { + return fmt.Errorf("could not parse kind ID as int: %s: %w", kindIDStr, err) + } + + kindMap, ok := ctx.scope.connKindMaps[connName] + if !ok { + // Try to fetch the kind map if the connection is open + var err error + kindMap, err = loadKindMap(ctx, connName) + if err != nil { + return fmt.Errorf("could not fetch kind map: %w", err) + } + } + + mapper := stubs.MapperFromKindMap(kindMap) + if kind, err := mapper.GetKindByID(int16(kindID)); err != nil { + return fmt.Errorf("could not look up kind: %w", err) + } else { + fmt.Fprintf(ctx.output, "Kind ID %d => %s", kindID, kind) + } + + return nil + }, + } +} diff --git a/tools/dawgrun/pkg/commands/help.go b/tools/dawgrun/pkg/commands/help.go new file mode 100644 index 0000000..7ef15a7 --- /dev/null +++ b/tools/dawgrun/pkg/commands/help.go @@ -0,0 +1,80 @@ +package commands + +import ( + "flag" + "fmt" + "maps" + "slices" + "strings" + + "github.com/mitchellh/go-wordwrap" +) + +func getFlagSetHelpDetails(flagSet *flag.FlagSet) string { + flagDefaults := new(strings.Builder) + oldOutput := flagSet.Output() + + flagSet.SetOutput(flagDefaults) + flagSet.PrintDefaults() + flagSet.SetOutput(oldOutput) + + return flagDefaults.String() +} + +func helpCmd() CommandDesc { + return CommandDesc{ + args: []string{"[command]"}, + help: "This help message, but also more detailed help for individual commands", + desc: "Get help for all commands", + + Fn: func(ctx *CommandContext, fields []string) error { + if len(fields) == 0 { + // HELP OVERVIEW + maxNameLength := 0 + for name := range cmdRegistry { + if nameLen := len(name); nameLen > maxNameLength { + maxNameLength = nameLen + } + } + + sortedCommands := slices.Sorted(maps.Keys(cmdRegistry)) + for _, name := range sortedCommands { + commandLeft := fmt.Sprintf( + "%s%s%s", + strings.Repeat(" ", 4), + name, + strings.Repeat(" ", maxNameLength-(len(name))), + ) + cmd := cmdRegistry[name] + spacerPad := strings.Repeat(" ", len(commandLeft)) + fmt.Fprintf(ctx.output, "%s%s%s\n", commandLeft, spacerPad, cmd.help) + } + } else { + // COMMAND HELP + name := fields[0] + cmd, ok := cmdRegistry[name] + if !ok { + return fmt.Errorf("unknown command: %s", name) + } + + wrappedHelp := indentLines(wordwrap.WrapString(cmd.help, 80), 1) + fmt.Fprintf(ctx.output, "\nHELP: %s %s\n\n", name, strings.Join(cmd.args, " ")) + fmt.Fprintf(ctx.output, "%s\n", wrappedHelp) + if strings.TrimSpace(cmd.desc) != "" { + fmt.Fprint(ctx.output, "\n") + fmt.Fprintf(ctx.output, "%s\n", indentLines(wordwrap.WrapString(cmd.desc, 80), 1)) + } + if cmd.flags != nil { + flagDefaults := getFlagSetHelpDetails(cmd.flags) + fmt.Fprintf(ctx.output, "\n%s\n%s\n", + indentLines("flags:", 1), + indentLines(wordwrap.WrapString(flagDefaults, 80), 2), + ) + } + fmt.Fprint(ctx.output, "END HELP\n") + } + + return nil + }, + } +} diff --git a/tools/dawgrun/pkg/commands/helpers.go b/tools/dawgrun/pkg/commands/helpers.go new file mode 100644 index 0000000..e32aa0e --- /dev/null +++ b/tools/dawgrun/pkg/commands/helpers.go @@ -0,0 +1,33 @@ +package commands + +import ( + "fmt" + "strings" + + "github.com/alecthomas/chroma/v2/quick" + cypherFrontend "github.com/specterops/dawgs/cypher/frontend" + cypherModels "github.com/specterops/dawgs/cypher/models/cypher" +) + +func parseQueryArray(fields []string) (*cypherModels.RegularQuery, error) { + cypherCtx := cypherFrontend.DefaultCypherContext() + return cypherFrontend.ParseCypher(cypherCtx, strings.Join(fields, " ")) +} + +func indentLines(text string, indentCount int) string { + builder := new(strings.Builder) + for line := range strings.Lines(text) { + fmt.Fprintf(builder, "%s%s", strings.Repeat("\t", indentCount), line) + } + + return builder.String() +} + +func highlightText(text, lexer, style string) (string, error) { + builder := new(strings.Builder) + if err := quick.Highlight(builder, text, lexer, "terminal256", style); err != nil { + return "", fmt.Errorf("could not highlight source text: %w", err) + } + + return builder.String(), nil +} diff --git a/tools/dawgrun/pkg/commands/registry.go b/tools/dawgrun/pkg/commands/registry.go new file mode 100644 index 0000000..bd21524 --- /dev/null +++ b/tools/dawgrun/pkg/commands/registry.go @@ -0,0 +1,33 @@ +// Package commands holds all of the repl commands along with infrastructure types and helpers +package commands + +import ( + "maps" + "slices" +) + +var cmdRegistry map[string]CommandDesc = map[string]CommandDesc{ + "exit": quitCmd(), + "explain-psql": explainAsPsqlCmd(), + "load-db-kinds": getPGDBKinds(), + "lookup-kind": lookupKindCmd(), + "lookup-kind-id": lookupKindIDCmd(), + "open-pg-db": openPGDBCmd(), + "parse": parseCmd(), + "query-cypher": queryCypherCmd(), + "quit": quitCmd(), + "runtime-trace": runtimeTraceCmd(), + "translate-psql": translateToPsqlCmd(), +} + +func init() { + cmdRegistry["help"] = helpCmd() +} + +func Registry() map[string]CommandDesc { + return cmdRegistry +} + +func SortedCommandNames() []string { + return slices.Sorted(maps.Keys(cmdRegistry)) +} diff --git a/tools/dawgrun/pkg/commands/runtime.go b/tools/dawgrun/pkg/commands/runtime.go new file mode 100644 index 0000000..c09ffa5 --- /dev/null +++ b/tools/dawgrun/pkg/commands/runtime.go @@ -0,0 +1,92 @@ +package commands + +import ( + "fmt" + "os" + "runtime/trace" + "strings" +) + +func quitCmd() CommandDesc { + return CommandDesc{ + args: []string{}, + help: "Quit", + desc: "Exits the REPL session", + + Fn: func(ctx *CommandContext, fields []string) error { + ctx.instance.Quit() + return nil + }, + } +} + +func runtimeTraceCmd() CommandDesc { + state := make(map[string]any) + state["run"] = false + state["tracefile"] = nil + + usage := "runtime-trace [tracefile]" + + return CommandDesc{ + args: []string{"start|stop", "[tracefile]"}, + help: "Manage runtime tracing", + desc: `start [tracefile] - Start tracing with output to [tracefile] if provided, otherwise trace.out +stop - Stop runtime tracing and close the trace file`, + + Fn: func(ctx *CommandContext, fields []string) error { + if len(fields) == 0 { + return fmt.Errorf("invalid usage: %s", usage) + } + subcmd := strings.ToLower(fields[0]) + switch subcmd { + case "start": + if running, ok := state["run"].(bool); ok && running { + return fmt.Errorf("runtime tracing is already enabled") + } + + var traceOut string + if len(fields) > 1 { + traceOut = fields[1] + } else { + traceOut = "trace.out" + } + + traceFile, err := os.Create(traceOut) + if err != nil { + return fmt.Errorf("error creating tracefile: %w", err) + } + + if err := trace.Start(traceFile); err != nil { + traceFile.Close() + return fmt.Errorf("could not start tracing: %w", err) + } + + state["run"] = true + state["tracefile"] = traceFile + + fmt.Fprintf(ctx.output, "Started runtime tracing to %s", traceOut) + return nil + case "stop": + if running, ok := state["run"].(bool); ok && !running { + return fmt.Errorf("runtime tracing is not running") + } + + trace.Stop() + traceFile, ok := state["tracefile"].(*os.File) + if !ok { + return fmt.Errorf("could not get open tracing file") + } + + traceFile.Close() + + state["run"] = false + state["tracefile"] = nil + + fmt.Fprint(ctx.output, "Stopped runtime tracing") + return nil + default: + return fmt.Errorf("invalid usage: %s", usage) + } + }, + } +} diff --git a/tools/dawgrun/pkg/commands/types.go b/tools/dawgrun/pkg/commands/types.go new file mode 100644 index 0000000..3c009c0 --- /dev/null +++ b/tools/dawgrun/pkg/commands/types.go @@ -0,0 +1,162 @@ +package commands + +import ( + "context" + "flag" + "fmt" + "io" + "os" + "strings" + "sync" + + "github.com/charmbracelet/lipgloss" + "github.com/openengineer/go-repl" + "github.com/specterops/dawgs/graph" + + "github.com/specterops/dawgs/tools/dawgrun/pkg/stubs" +) + +type ( + // CommandFn is the executable function for a single REPL command invocation. + CommandFn func(*CommandContext, []string) error + // CommandContext carries per-invocation context and shared command state. + CommandContext struct { + context.Context + + // instance is the running instance of the repl.Repl + instance *repl.Repl + // output is a convenience type to make issuing warnings and formatting outputs easier + output *CommandOutput + + // scope is a singleton instance held by the command manager that holds any persistent state for a command. + scope *Scope + } + // CommandDesc defines a command's behavior, arguments, and flag lifecycle. + CommandDesc struct { + // Fn is the command function to execute + Fn CommandFn + // ClearFlagsFn is used to clear a command's flags after execution + ClearFlagsFn func() + args []string + flags *flag.FlagSet + desc string + help string + state map[string]any + } + // CommandOutput accumulates output text and warnings for a command. + CommandOutput struct { + warnings []string + outputBuilder strings.Builder + } + // Scope holds persistent command state that can be shared across invocations. + Scope struct { + mu sync.RWMutex + connections map[string]graph.Database + connKindMaps map[string]stubs.KindMap + } +) + +// NewCommandContext creates a command context with a fresh output buffer. +func NewCommandContext(ctx context.Context, instance *repl.Repl, scope *Scope) *CommandContext { + return &CommandContext{ + Context: ctx, + output: new(CommandOutput), + instance: instance, + scope: scope, + } +} + +func (cc *CommandContext) warningStyle(text string) string { + return lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("202")). + Render(text) +} + +// OutputString renders warnings followed by command output text. +func (cc *CommandContext) OutputString() string { + builder := new(strings.Builder) + for _, warning := range cc.output.warnings { + fmt.Fprintf(builder, " * %s\n\n", cc.warningStyle(warning)) + } + + builder.WriteString(cc.output.outputBuilder.String()) + + return builder.String() +} + +var _ (io.Writer) = (*CommandOutput)(nil) + +// Warn appends a warning message to the command output. +func (co *CommandOutput) Warn(text string) { + co.warnings = append(co.warnings, text) +} + +// Warnf formats and appends a warning message to the command output. +func (co *CommandOutput) Warnf(text string, args ...any) { + co.warnings = append(co.warnings, fmt.Sprintf(text, args...)) +} + +// Write implements io.Writer for CommandOutput. +func (co *CommandOutput) Write(p []byte) (n int, err error) { + return co.outputBuilder.Write(p) +} + +// WriteIndented writes text after applying tab-based indentation per line. +func (co *CommandOutput) WriteIndented(text string, indentCount int) { + co.outputBuilder.WriteString(indentLines(text, indentCount)) +} + +// WriteHighlighted writes syntax-highlighted text using the configured style. +func (co *CommandOutput) WriteHighlighted(text, lexer string) { + style := os.Getenv("DAWGRUN_STYLE") + if style == "" { + style = "monokai" + } + + co.WriteHighlightedWithStyle(text, lexer, style) +} + +// WriteHighlightedWithStyle writes syntax-highlighted text with an explicit style. +func (co *CommandOutput) WriteHighlightedWithStyle(text, lexer, style string) { + highlighted, err := highlightText(text, lexer, style) + if err != nil { + co.Warnf("Could not highlight source text: %#v", err) + co.outputBuilder.WriteString(text) + } else { + co.outputBuilder.WriteString(highlighted) + } +} + +// NewScope creates an empty shared scope for command state. +func NewScope() *Scope { + return &Scope{ + connections: make(map[string]graph.Database), + connKindMaps: make(map[string]stubs.KindMap), + } +} + +// GetNumConnections returns the number of tracked database connections. +func (s *Scope) GetNumConnections() int { + s.mu.RLock() + defer s.mu.RUnlock() + + return len(s.connections) +} + +// AddConnection stores or replaces a named database connection in scope. +func (s *Scope) AddConnection(name string, querier graph.Database) { + s.mu.Lock() + defer s.mu.Unlock() + + s.connections[name] = querier +} + +// DropConnection removes a named connection and any cached kind map. +func (s *Scope) DropConnection(name string) { + s.mu.Lock() + defer s.mu.Unlock() + + delete(s.connections, name) + delete(s.connKindMaps, name) +} diff --git a/tools/dawgrun/pkg/go-repl/.gitignore b/tools/dawgrun/pkg/go-repl/.gitignore new file mode 100644 index 0000000..4f17582 --- /dev/null +++ b/tools/dawgrun/pkg/go-repl/.gitignore @@ -0,0 +1,6 @@ +main +go.sum +*.log +log.* +examples/basic_repl +examples/shell_wrapper diff --git a/tools/dawgrun/pkg/go-repl/LICENSE b/tools/dawgrun/pkg/go-repl/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/tools/dawgrun/pkg/go-repl/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/tools/dawgrun/pkg/go-repl/README.md b/tools/dawgrun/pkg/go-repl/README.md new file mode 100644 index 0000000..f154165 --- /dev/null +++ b/tools/dawgrun/pkg/go-repl/README.md @@ -0,0 +1,158 @@ +# go-repl + +## NOTICE + +Forked from https://github.com/OpenEngineer/go-repl @ 9990dd87c3fb9c039d3c2d1f00ceace8906b6644. +Changes applied: +- Added `repl.Options` to manage internal settings knobs +- Added the ability to render custom status bar components +- Support for OS-level paste into input line +- Added history load/save to file +- Added optional `repl.Completer` interface for richer tab completion + +## Original README + +Lightweight Golang REPL library, inspired by GNU Readline. You provide the `Eval` function, and `go-repl` does the rest. + +Your REPLs that use this library will enjoy the following features: +* Session history with *reverse-search* + * Ctrl-R to start *reverse-search* + * Most edit commands, except the most basic ones, exit the *reverse-search* mode + * Use Up/Down to cycle through a filtered list of history entries +* The input buffer is redrawn when a resize is detected +* Status bar at bottom with current working dir and other info +* Truncation of very long inputs (status bar displays info about cursor position) +* Common edit and movement commands: + * Right/Left: move cursor one character at a time + * Ctrl-Right/Left: move cursor one word at a time + * Up/Down: cycle through history + * Backspace/Delete: works as expected + * Shift-Enter: insert newline into buffer without invoking `Eval` + * Ctrl-A or Home: move to start of buffer + * Ctrl-E or End: move to end of buffer + * Ctrl-W: delete preceding word + * Ctrl-Q: delete following word + * Ctrl-C or Esc: ignore current input and reset buffer + * Ctrl-D: quit REPL + * Ctrl-U: clear buffer to start + * Ctrl-K: clear buffer to end + * Ctrl-L: reset prompt at top and redraw buffer + * Ctrl-Y: insert previous deletion (from Ctrl-K, Ctrl-U, Ctrl-Q or Ctrl-W) + +Notes: +* Doesn't depend on *ncurses* +* Performance hasn't yet been optimized and I haven't yet tested all corner cases exhaustively +* Might not work in Windows command prompt (keystroke codes could differ, ANSI escape sequences might not be supported, the method that sets terminal to raw mode might not work) +* No vi edit mode + +# Usage + +Fetch this library with the following command: +```shell +$ go get -u github.com/openengineer/go-repl +``` + +In order to create your own REPL you have to define a type that implements the `Handler` interface: +```golang +type Handler interface { + Prompt() string + Tab(buffer string) string + Eval(line string) string +} +``` + +If you want richer completion behavior (for example printing candidate lists), +you can optionally implement: + +```golang +type Completer interface { + Complete(buffer string) Completion +} +``` + +When `Completion.Candidates` is returned, go-repl renders them in a transient popover near the prompt. +By default it uses `overlay` mode. You can force `insert` mode with `REPL_COMPLETION_POPOVER_MODE=insert`. + +Here is a complete example (can also be found in `./examples/basic_repl.go`): + +```golang +package main + +import ( + "fmt" + "log" + "strconv" + "strings" + + repl "github.com/openengineer/go-repl" +) + +var helpMessage = `help display this message +add add two numbers +quit quit this program` + +// implements repl.Handler interface +type MyHandler struct { + r *repl.Repl +} + +func main() { + fmt.Println("Welcome, type \"help\" for more info") + + h := &MyHandler{} + h.r = repl.NewRepl(h) + + // start the terminal loop + if err := h.r.Loop(); err != nil { + log.Fatal(err) + } +} + +func (h *MyHandler) Prompt() string { + return "> " +} + +func (h *MyHandler) Tab(buffer string) string { + return "" // do nothing +} + +func (h *MyHandler) Eval(line string) string { + fields := strings.Fields(line) + + if len(fields) == 0 { + return "" + } else { + cmd, args := fields[0], fields[1:] + + switch cmd { + case "help": + return helpMessage + case "add": + if len(args) != 2 { + return "\"add\" expects 2 args" + } else { + return add(args[0], args[1]) + } + case "quit": + h.r.Quit() + return "" + default: + return fmt.Sprintf("unrecognized command \"%s\"", cmd) + } + } +} + +func add(a_ string, b_ string) string { + a, err := strconv.Atoi(a_) + if err != nil { + return "first arg is not an integer" + } + + b, err := strconv.Atoi(b_) + if err != nil { + return "second arg is not an integer" + } + + return strconv.Itoa(a + b) +} +``` diff --git a/tools/dawgrun/pkg/go-repl/ansi.go b/tools/dawgrun/pkg/go-repl/ansi.go new file mode 100644 index 0000000..76fd4c8 --- /dev/null +++ b/tools/dawgrun/pkg/go-repl/ansi.go @@ -0,0 +1,102 @@ +package repl + +import ( + "fmt" + "os" +) + +// TODO: dont use the functions that aren't supported by Windows + +const _ESC = "\033" + +func csi1(n int, char byte) { + fmt.Fprintf(os.Stdout, "%s[%d%c", _ESC, n, char) +} + +func csi2(n int, m int, char byte) { + fmt.Fprintf(os.Stdout, "%s[%d;%d%c", _ESC, n, m, char) +} + +func esc1(c byte) { + fmt.Fprintf(os.Stdout, "%s[%c", _ESC, c) +} + +func control(char byte) { + fmt.Fprintf(os.Stdout, "%c", char) +} + +func moveLeft() { + csi1(1, 'D') +} + +func moveRight() { + csi1(1, 'C') +} + +func clearScreen() { + csi1(2, 'J') +} + +func moveToRowStart() { + csi1(1, 'G') +} + +func moveToScreenStart() { + csi2(1, 1, 'H') +} + +func moveToRow(y int) { + csi2(y+1, 1, 'H') +} + +func clearRow() { + csi1(2, 'K') +} + +func clearRowAfterCursor() { + csi1(0, 'K') +} + +func clearRows(n int) { + for i := 0; i < n; i++ { + csi1(2, 'K') + + csi1(1, 'F') + } +} + +func insertLines(n int) { + if n > 0 { + csi1(n, 'L') + } +} + +func deleteLines(n int) { + if n > 0 { + csi1(n, 'M') + } +} + +// input: 0-based +// moves to 1-based +func moveToCol(x int) { + csi1(x+1, 'G') +} + +func queryCursorPos() { + csi1(6, 'n') +} + +// from 0-based to 1-based! +func moveCursorTo(x, y int) { + csi2(y+1, x+1, 'H') +} + +func highlight() { + // black text (30) on a grey/white background + fmt.Fprintf(os.Stdout, "%s[48;5;247m%s[30m", _ESC, _ESC) +} + +func resetDecorations() { + fmt.Fprintf(os.Stdout, "%s[0m", _ESC) +} diff --git a/tools/dawgrun/pkg/go-repl/go.mod b/tools/dawgrun/pkg/go-repl/go.mod new file mode 100644 index 0000000..3ff0a8b --- /dev/null +++ b/tools/dawgrun/pkg/go-repl/go.mod @@ -0,0 +1,8 @@ +module github.com/openengineer/go-repl + +go 1.15 + +require ( + github.com/openengineer/go-terminal v0.0.0-20220304032943-93486212aca4 // indirect + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 +) diff --git a/tools/dawgrun/pkg/go-repl/handler.go b/tools/dawgrun/pkg/go-repl/handler.go new file mode 100644 index 0000000..11d3b91 --- /dev/null +++ b/tools/dawgrun/pkg/go-repl/handler.go @@ -0,0 +1,25 @@ +package repl + +// Implement this interface in order to use `Repl` with your custom logic. +type Handler interface { + Prompt() string + Eval(buffer string) string + Tab(buffer string) string +} + +// Completion is the optional richer tab-completion response for Completer. +type Completion struct { + // Insert is appended to the buffer at the active cursor position. + Insert string + // Candidates are rendered as completion choices. + Candidates []string + // Message is rendered above Candidates when set. + Message string +} + +// Completer can be optionally implemented by a Handler to provide richer tab +// completion behavior (for example: candidate lists), while keeping Handler.Tab +// backwards-compatible. +type Completer interface { + Complete(buffer string) Completion +} diff --git a/tools/dawgrun/pkg/go-repl/justfile b/tools/dawgrun/pkg/go-repl/justfile new file mode 100644 index 0000000..c44e86d --- /dev/null +++ b/tools/dawgrun/pkg/go-repl/justfile @@ -0,0 +1,5 @@ +test: + go run ./examples/basic_repl/main.go + +test-shell-wrapper: + go run ./examples/shell_wrapper/main.go diff --git a/tools/dawgrun/pkg/go-repl/repl.go b/tools/dawgrun/pkg/go-repl/repl.go new file mode 100644 index 0000000..201bb50 --- /dev/null +++ b/tools/dawgrun/pkg/go-repl/repl.go @@ -0,0 +1,1920 @@ +// Lightweight Golang REPL library, inspired by GNU readline. You provide the Eval function, and go-repl does the rest. +package repl + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os" + "regexp" + "strconv" + "strings" + "sync" + "time" + + "golang.org/x/term" +) + +// Period between polls for terminal size changes. +// 10ms is the default, human reaction times are an order of magnitude slower than this interval, +// and auto generated escape sequence bytes are an order of magnitude faster than this interval. +var SIZE_POLLING_INTERVAL = 10 * time.Millisecond + +type ( + StatusWidgetFn func(*Repl) string + StatusWidgetFns struct { + Left StatusWidgetFn + Right StatusWidgetFn + } + completionPopover struct { + anchorRow int + height int + promptRow int + inserted bool + } +) + +type Repl struct { + handler Handler + + StatusWidgets *StatusWidgetFns + + history [][]byte // simply keep everything, it doesn't matter + historyIdx int // -1 for last + historyMaxLines int + historyFile *os.File // open history file, so we can keep appending + + phraseRe *regexp.Regexp + + reader *_StdinReader + + buffer []byte // input bytes are accumulated + backup []byte // we can go into a history line, and start editing it + prevDel []byte // previous deletion + filter []byte // for reverse search + bufferPos int // position in the buffer (0-based) + viewStart int // usually 0, but can be positive in case of very large inputs + viewEnd int // + promptRow int // 0-based + width int + height int + + frameLock sync.Mutex + bufLock sync.Mutex + + completionPopover *completionPopover + + onEnd func() + debug *os.File +} + +type Options struct { + // HistoryFilePath is the path to the file that console history is stored in + HistoryFilePath string + // HistoryFileMaxLines is the maximum number of lines of the history that should be kept + // before it gets truncated + HistoryMaxLines int + // StatusWidgets is a struct containing widgets used for content when rendering the status + // bar to screen + StatusWidgets *StatusWidgetFns +} + +// Create a new Repl using your custom Handler. +func NewRepl(handler Handler, opts *Options) *Repl { + r := &Repl{ + handler: handler, + history: make([][]byte, 0), + historyIdx: -1, + historyFile: nil, + historyMaxLines: 1000, + phraseRe: regexp.MustCompile(`([0-9a-zA-Z_\-\.]+)`), + reader: newStdinReader(), + buffer: nil, + backup: nil, + prevDel: nil, + filter: nil, + bufferPos: 0, + viewStart: 0, + viewEnd: -1, + promptRow: -1, + width: 0, + height: 0, + completionPopover: nil, + onEnd: nil, + debug: nil, + } + + debug := os.Getenv("REPL_DEBUG_LOG") + if debug != "" { + debug, err := os.Create(debug) + if err != nil { + panic(fmt.Errorf("error start repl (debug): %w", err)) + } + r.debug = debug + } + + if opts != nil { + if err := r.loadOptions(opts); err != nil { + panic(fmt.Errorf("error starting repl (options): %w", err)) + } + } + + return r +} + +/////////////////// +// internal methods +/////////////////// + +func (r *Repl) loadOptions(opts *Options) error { + if opts.HistoryFilePath != "" { + // Open history file from HistoryFilePath + historyFile, err := os.OpenFile(opts.HistoryFilePath, os.O_CREATE|os.O_RDWR|io.SeekStart, 0o660) + if err != nil { + return fmt.Errorf("could not open history file: %w", err) + } + + r.historyFile = historyFile + + // Load history file into the buffer + if err := r.loadHistory(); err != nil { + return fmt.Errorf("could not load history from file: %w", err) + } + } + + if opts.HistoryMaxLines > 0 { + r.historyMaxLines = opts.HistoryMaxLines + } + + r.StatusWidgets = opts.StatusWidgets + + return nil +} + +func (r *Repl) loadHistory() error { + historyReader := bufio.NewReader(r.historyFile) + reading := true + for reading { + line, err := historyReader.ReadBytes('\n') + if err != nil { + switch err { + case io.EOF: + reading = false + default: + return fmt.Errorf("could not read from history file: %w", err) + } + } + + if line == nil { + continue + } + + r.history = append(r.history, bytes.TrimSpace(line)) + } + + return nil +} + +func (r *Repl) saveHistory() error { + if r.historyFile == nil { + return nil + } + + if err := r.historyFile.Truncate(0); err != nil { + return fmt.Errorf("could not truncate history file: %w", err) + } + + if _, err := r.historyFile.Seek(0, io.SeekStart); err != nil { + return fmt.Errorf("could not seek to beginning after truncate: %w", err) + } + + // truncate history buffer before save. + if len(r.history) > r.historyMaxLines { + r.history = r.history[len(r.history)-r.historyMaxLines:] + } + + historyWriter := bufio.NewWriter(r.historyFile) + for _, line := range r.history { + line := bytes.TrimSpace(line) + // Filter blank lines + if len(line) == 0 { + continue + } + + if n, err := historyWriter.Write(append(line, byte('\n'))); n < len(line) || err != nil { + return fmt.Errorf("could not write history to file (%d bytes written): %w", n, err) + } + } + + if err := historyWriter.Flush(); err != nil { + return fmt.Errorf("could not flush writer: %w", err) + } + + if err := r.historyFile.Close(); err != nil { + return fmt.Errorf("could not close history file: %w", err) + } + + return nil +} + +func (r *Repl) getWidth() int { + r.frameLock.Lock() + defer r.frameLock.Unlock() + + return r.width +} + +func (r *Repl) getHeight() int { + r.frameLock.Lock() + defer r.frameLock.Unlock() + + return r.height +} + +func (r *Repl) innerHeight() int { + if r.statusVisible() { + return r.getHeight() - 1 + } else { + return r.getHeight() + } +} + +func (r *Repl) log(format string, args ...interface{}) { + if r.debug != nil { + fmt.Fprintf(r.debug, format, args...) + } +} + +func getTermSize() (int, int, error) { + w, h, err := term.GetSize(0) + if err != nil { + return 0, 0, fmt.Errorf("could not get size; not an interactive terminal?: %w", err) + } + + return w, h, nil +} + +func (r *Repl) notifySizeChange() { + if width, height, err := getTermSize(); err != nil { + return + } else { + r.frameLock.Lock() + r.width = width + r.height = height + r.frameLock.Unlock() + } + + go func() { + ticker := time.NewTicker(SIZE_POLLING_INTERVAL) + defer ticker.Stop() + + for { + <-ticker.C + + newW, newH, err := getTermSize() + if err != nil { + continue + } + r.resize(newW, newH) + } + }() +} + +func (r *Repl) resize(w, h int) { + if w != r.width || h != r.height { + if r.completionPopoverVisible() { + r.clearCompletionPopover() + } + + r.frameLock.Lock() + r.width, r.height = w, h + r.frameLock.Unlock() + + r.force(r.buffer, r.bufferPos) + } +} + +func (r *Repl) searchActive() bool { + return r.filter != nil +} + +func (r *Repl) stopSearch() { + r.filter = nil + + r.clearStatus() + r.writeStatus() +} + +func (r *Repl) completionPopoverVisible() bool { + return r.completionPopover != nil +} + +func (r *Repl) completionPopoverMode() string { + switch strings.ToLower(strings.TrimSpace(os.Getenv("REPL_COMPLETION_POPOVER_MODE"))) { + case "insert": + return "insert" + } + + return "overlay" +} + +func isCursorQueryResponse(b []byte) bool { + n := len(b) + if n < 3 || b[n-1] != 82 { + return false + } + + for i := n - 1; i >= 1; i-- { + if b[i-1] == 27 && b[i] == 91 { + return true + } + } + + return false +} + +func (r *Repl) clearCompletionPopover() { + popover := r.completionPopover + if popover == nil { + return + } + + r.completionPopover = nil + + if popover.height <= 0 { + return + } + + if popover.inserted { + if r.statusVisible() { + moveCursorTo(0, r.getHeight()-1) + clearRow() + } + + moveCursorTo(0, popover.anchorRow) + deleteLines(popover.height) + } else { + for i := 0; i < popover.height; i++ { + moveCursorTo(0, popover.anchorRow+i) + clearRow() + } + } + + r.updatePromptRow(popover.promptRow) +} + +func (r *Repl) dismissCompletionPopover() { + if !r.completionPopoverVisible() { + return + } + + r.clearCompletionPopover() + r.syncCursor() + r.writeStatus() +} + +// turn stdin bytes into something useful +func (r *Repl) dispatch(b []byte) { + n := len(b) + + r.log("keypress: %v\n", b) + + if r.completionPopoverVisible() { + if n == 1 && b[0] == 9 { + // keep visible for repeated tab completions + } else if n == 1 && b[0] == 27 { + r.dismissCompletionPopover() + return + } else if isCursorQueryResponse(b) { + // keep visible while handling terminal cursor query replies + } else { + r.dismissCompletionPopover() + } + } + + if n == 1 { + switch b[0] { + case 0: // NULL, or CTRL-2 + return + case 1: // CTRL-A + r.moveToBufferStart() + case 2: // CTRL-B + r.moveLeftOneChar() + case 3: // CTRL-C + if r.searchActive() { + r.stopSearch() + } + + r.clearBuffer() + r.writeStatus() + case 4: // CTRL-D + r.quit() + case 5: // CTRL-E + r.moveToBufferEnd() + case 6: // CTRL-F + r.moveRightOneChar() + case 8: // CTRL-H + r.backspaceActiveBuffer() + case 9: // TAB + if r.searchActive() { + r.stopSearch() + } else { + r.tab() + } + case 10: // SHIFT-ENTER + if r.searchActive() { + r.stopSearch() + } else { + r.clearStatus() + r.addBytesToBuffer([]byte{'\n'}) + r.writeStatus() + } + case 11: // CTRL-K + if r.searchActive() { + r.stopSearch() + } else { + r.clearToEnd() + } + case 12: // CTRL-L + r.redrawScreen() + case 13: // RETURN + if r.searchActive() { + r.stopSearch() + } else { + r.evalBuffer() + } + case 14: // CTRL-N + r.historyForward() + case 16: // CTRL-P + r.historyBack() + case 17: // CTRL-Q + if r.searchActive() { + r.stopSearch() + } else { + r.clearOnePhraseRight() + } + case 18: // CTRL-R + if !r.searchActive() { + r.startReverseSearch() + } + case 21: // CTRL-U + if r.searchActive() { + r.stopSearch() + } else { + r.clearToStart() + } + case 22: // CTRL-V + return + case 25: // CTRL-Y + if r.searchActive() { + r.stopSearch() + } else { + r.clearStatus() + r.insertPrevDel() + r.writeStatus() + } + case 23: // CTRL-W + if r.searchActive() { + r.stopSearch() + } else { + r.clearOnePhraseLeft() + } + case 27: // ESC + if r.searchActive() { + r.stopSearch() + } else { + r.clearBuffer() + r.writeStatus() + } + case 127: // BACKSPACE + r.backspaceActiveBuffer() + default: + if b[0] >= 32 { + if r.searchActive() { + r.filter = append(r.filter, b[0]) + + r.updateSearchResult() + } else { + r.clearStatus() + r.addBytesToBuffer([]byte{b[0]}) + } + r.writeStatus() + } + } + } else if n == 2 && b[0] == 195 { + // ALT + KEY + } else if n > 2 && b[0] == 27 && b[1] == 79 { // [ESCAPE, O, ...] + switch b[2] { + case 80: // F1 + case 81: // F2 + // ... + default: + // function keys not yet supported + } + } else if n > 2 && b[0] == 27 && b[1] == 91 { // [ESCAPE, OPEN_BRACKET, ...] + if n == 3 { + switch b[2] { + case 65: + r.historyBack() + case 66: + r.historyForward() + case 67: // ArrowRight + r.moveRightOneChar() + case 68: // ArrowLeft + r.moveLeftOneChar() + case 72: + r.moveToBufferStart() + case 70: + r.moveToBufferEnd() + } + } else if n == 4 { + if b[2] == 51 && b[3] == 126 { + r.deleteChar() + } + } else if n == 6 && b[2] == 49 && b[3] == 59 { + if b[4] == 53 && b[5] == 68 { // CTRL-ArrowLeft + r.moveLeftOnePhrase() + } else if b[4] == 53 && b[5] == 67 { + r.moveRightOnePhrase() + } else if b[4] == 53 && b[5] == 66 { + // r.moveDownOneLine() + } else if b[4] == 53 && b[5] == 65 { + // r.moveUpOneLine() + } + } else if len(b) > 5 && b[n-1] == 82 { + parts := strings.Split(string(b[2:n-1]), ";") + row, err := strconv.Atoi(parts[0]) + if err == nil { + col, err := strconv.Atoi(parts[1]) + if err == nil { + r.handleCursorQuery(col-1, row-1) + } + } + } + } else if len(b) > 6 && b[n-1] == 82 { + // go backwards until the esc char + for i := n - 2; i >= 0; i-- { + if b[i] == 27 && b[i+1] == 91 { + parts := strings.Split(string(b[i+2:n-1]), ";") + row, err := strconv.Atoi(parts[0]) + if err == nil { + col, err := strconv.Atoi(parts[1]) + if err == nil { + r.handleCursorQuery(col-1, row-1) + } + } + + printable := make([]byte, 0) + for _, b_ := range b[0:i] { + if b_ >= 32 { + printable = append(printable, b_) + } + } + + if len(printable) > 0 { + r.clearStatus() + r.addBytesToBuffer(printable) + r.writeStatus() + } + + break + } + } + } else { + r.cleanAndAddToBuffer(b) + } + + return +} + +func (r *Repl) handleCursorQuery(x, y int) { + r.updatePromptRow(y) + + r.writeStatus() +} + +func (r *Repl) printPrompt() { + moveToRowStart() + fmt.Print(r.handler.Prompt()) +} + +func (r *Repl) resetBuffer() { + r.bufLock.Lock() + defer r.bufLock.Unlock() + + r.bufferPos = 0 + r.buffer = make([]byte, 0) + r.printPrompt() + r.viewStart = 0 + r.viewEnd = -1 +} + +func (r *Repl) overflow() bool { + b := r.calcHeight() > r.innerHeight() + if !b { + r.viewStart = 0 + r.viewEnd = -1 + } + return b +} + +func (r *Repl) viewOverflow() bool { + return r.calcViewHeight() > r.innerHeight() +} + +func (r *Repl) boundPromptRow() { + n := r.viewEnd + if n < 0 { + n = r.bufferLen() + } + + xe, ye := r.cursorCoord(n) + + if ye >= r.innerHeight() { + moveCursorTo(xe, ye) + fmt.Print("\n") + r.updatePromptRow(r.promptRow - (ye + 1 - r.innerHeight())) + } +} + +func (r *Repl) addBytesToBuffer(bs []byte) { + if r.bufferPos == r.bufferLen() { + xBef, _ := r.cursorCoord(-1) + + r.bufferPos += len(bs) + len_ := r.bufferLen() + r.buffer = append(r.buffer, bs...) + + if !r.overflow() { + needSync := false + for _, b := range bs { + r.writeByte(b) + + if b != '\n' && xBef == r.getWidth()-1 { + needSync = true + } + } + + if needSync { + r.syncCursor() + } + + r.boundPromptRow() + + return + } else { + // reset prev changes + r.bufferPos -= len(bs) + r.buffer = r.buffer[0:len_] + } + } + + tail := r.buffer[r.bufferPos:] + + newBuffer := make([]byte, 0) + newBuffer = append(newBuffer, r.buffer[0:r.bufferPos]...) + newBuffer = append(newBuffer, bs...) + newBuffer = append(newBuffer, tail...) + + newPos := r.bufferPos + len(bs) + + r.force(newBuffer, newPos) // force should take into account extra long lines +} + +func (r *Repl) promptLen() int { + return len(r.handler.Prompt()) +} + +func (r *Repl) bufferLen() int { + return len(r.buffer) +} + +func relCursorCoord(buffer []byte, x0 int, bufferPos int, w int) (int, int) { + x := x0 + y := 0 + + for j, c := range buffer { + if j >= bufferPos { + break + } else if c == '\n' { + x = 0 + y += 1 + } else { + x += 1 + } + + if x == w { + x = 0 + y += 1 + } + } + + return x, y +} + +func calcHeight(buffer []byte, x0 int, w int) int { + _, y := relCursorCoord(buffer, x0, len(buffer), w) + return y + 1 +} + +func (r *Repl) calcHeight() int { + return calcHeight(r.buffer, r.promptLen(), r.getWidth()) +} + +func (r *Repl) calcViewHeight() int { + if r.viewEnd > r.bufferLen() { + r.viewEnd = r.bufferLen() + } + + return calcHeight(r.buffer[r.viewStart:r.viewEnd], r.promptLen(), r.getWidth()) +} + +func (r *Repl) calcViewStartHeight() int { + return calcHeight(r.buffer[0:r.viewStart], r.promptLen(), r.getWidth()) +} + +func (r *Repl) calcViewEndHeight() int { + return r.calcHeight() - r.calcViewHeight() +} + +// i is 0-based index in current buffer +func (r *Repl) cursorCoord(bufferPos int) (int, int) { + w := r.getWidth() + + if bufferPos < 0 { + bufferPos = r.bufferPos + } + + x, y := relCursorCoord(r.buffer[r.viewStart:], r.promptLen(), bufferPos-r.viewStart, w) + + y += r.promptRow + + return x, y +} + +// return bufferPos that matches (x,y) as best as possible +func (r *Repl) calcBufferPos(x, y int) int { + xc := r.promptLen() + yc := r.promptRow + + for i, c := range r.buffer[r.viewStart:] { + if yc > y { + r.log("overshoot\n") + return i - 1 + r.viewStart + } else if yc == y && xc >= x { + r.log("calc pos for %d,%d -> %d (%d,%d)\n", x, y, i+r.viewStart, xc, yc) + return i + r.viewStart + } + + if c == '\n' { + xc = 0 + yc += 1 + } else { + xc += 1 + } + + if xc == r.getWidth() { + xc = 0 + yc += 1 + } + + } + + if r.viewEnd >= 0 { + return r.viewEnd + } else { + return r.bufferLen() + } +} + +func (r *Repl) clearAfterPrompt() { + moveCursorTo(0, r.getHeight()-1) + + if r.promptRow < 0 { + r.updatePromptRow(0) + } + + dy := (r.getHeight() - 1 - r.promptRow) + + clearRows(dy) +} + +// clear as much as possible +func (r *Repl) clearBuffer() { + moveCursorTo(0, r.getHeight()-1) + + r.log("clearing buffer\n") + if r.promptRow < 0 { + r.updatePromptRow(0) + } + + dy := (r.getHeight() - 1 - r.promptRow) + + clearRows(dy) + clearRow() + + r.resetBuffer() +} + +func copyBytes(b []byte) []byte { + l := make([]byte, len(b)) + + for i, c := range b { + l[i] = c + } + + return l +} + +func (r *Repl) adjustBufferView() { + if r.bufferPos < r.viewStart { + r.viewStart = r.bufferPos + r.viewEnd = r.bufferLen() + + for r.viewOverflow() { + r.viewEnd -= 1 + } + } else if r.bufferPos > r.viewEnd { + r.viewEnd = r.bufferPos + for r.viewOverflow() { + r.viewStart += 1 + } + } else if r.viewOverflow() { + r.viewEnd = r.bufferLen() + + for r.viewOverflow() { + r.viewEnd -= 1 + } + } else { + for !r.viewOverflow() && r.viewEnd < r.bufferLen() { + r.viewEnd += 1 + } + + for r.viewOverflow() { + r.viewEnd -= 1 + } + } +} + +// this works for a single line +func (r *Repl) force(newBuffer []byte, bufferPos int) { + newBuffer = copyBytes(newBuffer) + + r.clearStatus() + + r.log("overflow? %d vs %d\n", calcHeight(newBuffer, r.promptLen(), r.getWidth()), r.innerHeight()) + if calcHeight(newBuffer, r.promptLen(), r.getWidth()) > r.innerHeight() { + viewStart_, viewEnd_ := r.viewStart, r.viewEnd + r.clearScreen() + + r.bufLock.Lock() + r.buffer = newBuffer + r.bufferPos = bufferPos + r.viewStart, r.viewEnd = viewStart_, viewEnd_ + r.bufLock.Unlock() + + r.log("viewStart: %d, viewEnd: %d\n", r.viewStart, r.viewEnd) + r.adjustBufferView() + + r.log("writing bytes from %d to %d (instead of 0 to %d) (bpos: %d)\n", r.viewStart, r.viewEnd, r.bufferLen(), r.bufferPos) + + for _, b := range r.buffer[r.viewStart:r.viewEnd] { + r.writeByte(b) + } + + r.syncCursor() + // what is the appropriate bufferOffset? The minimal movement to keep the /move + } else { + r.clearBuffer() + + // TODO: writeBytes instead + r.addBytesToBuffer(newBuffer) + + r.bufLock.Lock() + if bufferPos >= r.bufferLen() { + bufferPos = r.bufferLen() + } + + r.bufferPos = bufferPos + r.bufLock.Unlock() + + r.log("bufferPos: %d, bufferLen: %d, width: %d\n", r.bufferPos, len(r.buffer), r.getWidth()) + r.syncCursor() + } + + r.writeStatus() +} + +func (r *Repl) syncCursor() { + x, y := r.cursorCoord(-1) + moveCursorTo(x, y) +} + +func (r *Repl) evalBuffer() { + r.clearStatus() + + r.newLine() + + // input that is sent to stdin while the handler is blocking, is returned the next time we read bytes from the stdinreader, followed by a sequence indicating the new cursor position (due to queryCursorPos() being called below), so the routine that handles the cursor pos query should also handle any preceding bytes + out := r.handler.Eval(strings.TrimSpace(string(r.buffer))) + + if len(out) > 0 { + outLines := strings.Split(out, "\n") + + for _, outLine := range outLines { + fmt.Print(outLine) + r.newLine() + } + } + + r.appendToHistory(r.buffer) + r.historyIdx = -1 + + r.backup = nil + + r.resetBuffer() + + queryCursorPos() +} + +func (r *Repl) redraw() { + r.force(r.buffer, r.bufferPos) +} + +func (r *Repl) syncCursorOverflow() { + if r.overflow() { + r.redraw() + } else { + r.syncCursor() + } +} + +func (r *Repl) moveToBufferEnd() { + if r.searchActive() { + r.stopSearch() + } else { + r.bufferPos = r.bufferLen() + + r.syncCursorOverflow() + } +} + +func (r *Repl) moveToBufferStart() { + if r.searchActive() { + r.stopSearch() + } else { + r.bufferPos = 0 + + r.syncCursorOverflow() + } +} + +func (r *Repl) moveLeftOneChar() { + if r.searchActive() { + r.stopSearch() + } else { + if r.bufferPos > 0 { + r.bufferPos -= 1 + + if r.overflow() { + if r.bufferPos <= r.viewStart { + r.redraw() + return + } + } + + r.syncCursor() + } + } +} + +func (r *Repl) moveRightOneChar() { + if r.searchActive() { + r.stopSearch() + } else { + if r.bufferPos < r.bufferLen() { + r.bufferPos += 1 + + if r.overflow() { + if r.bufferPos >= r.viewEnd { + r.redraw() + return + } + } + + r.syncCursor() + } + } +} + +func (r *Repl) moveUpOneLine() { + x, y := r.cursorCoord(-1) + + h0 := r.calcViewStartHeight() + _, y0 := r.cursorCoord(r.viewStart) + + if ((h0 > 0) && (y >= y0)) || y > y0 { + // problem is that y is in view space, and + r.bufferPos = r.calcBufferPos(x, y-1) + + if r.overflow() { + if r.bufferPos <= r.viewStart { + r.redraw() + return + } + } + + r.syncCursor() + } +} + +func (r *Repl) moveDownOneLine() { + x, y := r.cursorCoord(-1) + + _, ye := r.cursorCoord(r.viewEnd) + he := r.calcViewEndHeight() + + if y < ye || (y <= ye && he > 0) { + r.bufferPos = r.calcBufferPos(x, y+1) + + if r.overflow() { + if r.bufferPos >= r.viewEnd { + r.redraw() + return + } + } + + r.syncCursor() + } +} + +func (r *Repl) moveLeftOnePhrase() { + newPos, ok := r.prevPhrasePos() + if ok { + r.bufferPos = newPos + + if r.overflow() { + if r.bufferPos <= r.viewStart { + r.redraw() + return + } + } + + r.syncCursor() + } +} + +func (r *Repl) moveRightOnePhrase() { + newPos, ok := r.nextPhrasePos() + if ok { + r.bufferPos = newPos + + if r.overflow() { + if r.bufferPos >= r.viewEnd { + r.redraw() + return + } + } + + r.syncCursor() + } +} + +// dont append if the same as the previous +func (r *Repl) appendToHistory(entry []byte) { + n := len(r.history) + + if n == 0 { + r.history = append(r.history, entry) + } else if string(r.history[n-1]) != string(entry) { + r.history = append(r.history, entry) + } +} + +func (r *Repl) useHistoryEntry(i int) { + if i == -1 { + r.historyIdx = -1 + + if r.backup != nil { + r.force(r.backup, len(r.backup)) + } + + r.backup = nil + } else { + if r.backup == nil { + r.backup = r.buffer + } + + r.historyIdx = i + + entry := r.history[i] + + r.force(entry, len(entry)) + } +} + +func (r *Repl) historyForward() { + if r.searchActive() { + if r.historyIdx >= 0 && r.historyIdx < len(r.history)-1 { + for i := r.historyIdx + 1; i < len(r.history); i++ { + if r.filterMatches(r.history[i]) { + r.useHistoryEntry(i) + return + } + } + } + } else { + if r.historyIdx != -1 { + if r.historyIdx < len(r.history)-1 { + r.useHistoryEntry(r.historyIdx + 1) + } else { + r.useHistoryEntry(-1) + } + } + } +} + +func (r *Repl) historyBack() { + if r.searchActive() { + if r.historyIdx > 0 { + for i := r.historyIdx - 1; i >= 0; i-- { + if r.filterMatches(r.history[i]) { + r.useHistoryEntry(i) + return + } + } + } + } else { + if r.historyIdx == -1 { + if len(r.history) > 0 { + r.useHistoryEntry(len(r.history) - 1) + } + } else if r.historyIdx > 0 { + r.useHistoryEntry(r.historyIdx - 1) + } + } +} + +func (r *Repl) startReverseSearch() { + r.filter = make([]byte, 0) + + r.clearStatus() + r.writeStatus() +} + +func truncateCompletionLine(line string, maxWidth int) string { + if maxWidth <= 0 { + return "" + } + + if len(line) <= maxWidth { + return line + } + + if maxWidth <= 3 { + return line[0:maxWidth] + } + + return line[0:maxWidth-3] + "..." +} + +func completionPopoverLines(completion Completion) []string { + lines := make([]string, 0, len(completion.Candidates)+1) + if completion.Message != "" { + lines = append(lines, completion.Message) + } + + lines = append(lines, completion.Candidates...) + + const maxContentLines = 12 + if len(lines) > maxContentLines { + if maxContentLines == 1 { + lines = []string{fmt.Sprintf("%d matches", len(lines))} + } else { + hidden := len(lines) - (maxContentLines - 1) + lines = append(lines[0:maxContentLines-1], fmt.Sprintf("... %d more", hidden)) + } + } + + return lines +} + +func clampCompletionLines(lines []string, maxContentRows int) []string { + if maxContentRows <= 0 { + return nil + } + + if len(lines) <= maxContentRows { + return lines + } + + if maxContentRows == 1 { + return []string{fmt.Sprintf("%d matches", len(lines))} + } + + hidden := len(lines) - (maxContentRows - 1) + return append(lines[0:maxContentRows-1], fmt.Sprintf("... %d more", hidden)) +} + +func (r *Repl) completionRowsBelowPrompt() int { + statusRow := r.getHeight() + if r.statusVisible() { + statusRow = r.getHeight() - 1 + } + + return statusRow - (r.promptRow + 1) +} + +func (r *Repl) bumpScrollback(lines int) { + if lines <= 0 { + return + } + + if r.statusVisible() { + moveCursorTo(0, r.getHeight()-1) + clearRow() + } + + for i := 0; i < lines; i++ { + moveCursorTo(0, r.getHeight()-1) + r.newLine() + } + + r.updatePromptRow(r.promptRow - lines) +} + +func (r *Repl) showCompletionPopover(completion Completion) { + lines := completionPopoverLines(completion) + if len(lines) == 0 || r.getWidth() < 8 { + return + } + + if r.promptRow < 0 { + queryCursorPos() + return + } + + if r.completionPopoverVisible() { + r.clearCompletionPopover() + } + + maxContentWidth := r.getWidth() - 4 // `| ` + ` |` + if maxContentWidth <= 0 { + return + } + + availableBelow := r.completionRowsBelowPrompt() + maxPossibleBelow := availableBelow + r.promptRow + if maxPossibleBelow < 3 { + return + } + + inserted := r.completionPopoverMode() == "insert" + maxContentRows := maxPossibleBelow - 2 + + lines = clampCompletionLines(lines, maxContentRows) + if len(lines) == 0 { + return + } + + maxLineLen := 0 + for idx, line := range lines { + line = truncateCompletionLine(line, maxContentWidth) + lines[idx] = line + + if len(line) > maxLineLen { + maxLineLen = len(line) + } + } + + if maxLineLen <= 0 { + maxLineLen = 1 + } + + height := len(lines) + 2 + anchorRow := r.promptRow + 1 + + if availableBelow < height { + deficit := height - availableBelow + r.bumpScrollback(deficit) + anchorRow = r.promptRow + 1 + } + + if inserted { + r.clearStatus() + + moveCursorTo(0, anchorRow) + insertLines(height) + } + + moveCursorTo(0, anchorRow) + clearRow() + // border top + fmt.Print("┌" + strings.Repeat("─", maxLineLen+2) + "┐") + + for idx, line := range lines { + moveCursorTo(0, anchorRow+1+idx) + clearRow() + fmt.Print("│ ") + fmt.Print(line) + + if padding := maxLineLen - len(line); padding > 0 { + fmt.Print(strings.Repeat(" ", padding)) + } + + fmt.Print(" │") + } + + moveCursorTo(0, anchorRow+height-1) + clearRow() + // border bottom + fmt.Print("└" + strings.Repeat("─", maxLineLen+2) + "┘") + + r.completionPopover = &completionPopover{ + anchorRow: anchorRow, + height: height, + promptRow: r.promptRow, + inserted: inserted, + } + + r.syncCursor() + r.writeStatus() +} + +func (r *Repl) tab() { + prec := string(r.buffer[0:r.bufferPos]) + + if completer, ok := r.handler.(Completer); ok { + completion := completer.Complete(prec) + + if len(completion.Insert) > 0 { + if r.completionPopoverVisible() { + r.dismissCompletionPopover() + } + + r.addBytesToBuffer([]byte(completion.Insert)) + r.writeStatus() + return + } + + if len(completion.Message) > 0 || len(completion.Candidates) > 0 { + r.showCompletionPopover(completion) + } else if r.completionPopoverVisible() { + r.dismissCompletionPopover() + } + + return + } + + if r.completionPopoverVisible() { + r.dismissCompletionPopover() + } + + extra := r.handler.Tab(prec) + + if len(extra) > 0 { + r.addBytesToBuffer([]byte(extra)) + r.writeStatus() + } +} + +func (r *Repl) quit() { + r.clearAfterPrompt() + if err := r.saveHistory(); err != nil { + r.log("could not save history: %v", err) + } + + fmt.Print("\n\r") + + moveToRowStart() + + r.UnmakeRaw() + + os.Exit(0) +} + +func (r *Repl) redrawScreen() { + buffer := r.buffer + bufferPos := r.bufferPos + + r.clearScreen() + + r.force(buffer, bufferPos) +} + +func (r *Repl) clearScreen() { + clearScreen() + + moveToScreenStart() + + r.updatePromptRow(0) + + r.resetBuffer() +} + +func (r *Repl) backspaceActiveBuffer() { + if r.searchActive() { + n := len(r.filter) + if n > 0 { + r.filter = r.filter[0 : n-1] + } + + r.updateSearchResult() + + r.clearStatus() + r.writeStatus() + } else { + r.backspace() + } +} + +func (r *Repl) backspace() { + n := r.bufferLen() + + if n > 0 { + if r.bufferPos > 0 { + newPos := r.bufferPos - 1 + newBuffer := append(r.buffer[0:newPos], r.buffer[newPos+1:len(r.buffer)]...) + + _, y0 := r.cursorCoord(-1) + x1, y1 := r.cursorCoord(newPos) + + if y0 == y1 && r.bufferPos == len(r.buffer) && !r.overflow() { + moveToCol(x1) + clearRowAfterCursor() + r.buffer = newBuffer + r.bufferPos = newPos + } else { + r.force(newBuffer, newPos) + } + } + } +} + +func (r *Repl) deleteChar() { + if r.searchActive() { + r.stopSearch() + } else { + if r.bufferPos < r.bufferLen() { + newBuffer := make([]byte, 0) + newBuffer = append(newBuffer, r.buffer[0:r.bufferPos]...) + + if r.bufferPos < r.bufferLen()-1 { + newBuffer = append(newBuffer, r.buffer[r.bufferPos+1:]...) + } + + newPos := r.bufferPos + + r.force(newBuffer, newPos) + } + } +} + +func (r *Repl) clearToEnd() { + if r.bufferPos != r.bufferLen() { + newBuffer := r.buffer[0:r.bufferPos] + + r.prevDel = r.buffer[r.bufferPos:] + + r.force(newBuffer, r.bufferPos) + } +} + +func (r *Repl) clearToStart() { + if r.bufferPos > 0 { + newBuffer := r.buffer[r.bufferPos:] + + r.prevDel = r.buffer[0:r.bufferPos] + + r.force(newBuffer, 0) + } +} + +func (r *Repl) phraseStartPositions() []int { + if len(r.buffer) == 0 { + return []int{0} + } + + re := r.phraseRe + + indices := re.FindAllIndex(r.buffer, -1) + + res := make([]int, 0) + + for i, match := range indices { + start := match[0] + stop := match[1] + if i == 0 && start != 0 { + res = append(res, 0) + } + + res = append(res, start, stop) + + if i == len(indices)-1 && stop != len(r.buffer) { + res = append(res, len(r.buffer)) + } + } + + if len(res) == 0 || res[len(res)-1] != len(r.buffer) { + res = append(res, len(r.buffer)) + } + + return res +} + +func (r *Repl) nextPhrasePos() (int, bool) { + var res int + if r.bufferPos == r.bufferLen() { + res = r.bufferLen() + } else { + indices := r.phraseStartPositions() + + for _, idx := range indices { + if idx > r.bufferPos { + res = idx + break + } + } + } + + return res, res != r.bufferPos +} + +func (r *Repl) prevPhrasePos() (int, bool) { + var res int + if r.bufferPos == 0 { + res = 0 + } else { + indices := r.phraseStartPositions() + + for i := len(indices) - 1; i >= 0; i-- { + idx := indices[i] + if idx < r.bufferPos { + res = idx + break + } + } + } + + return res, res != r.bufferPos +} + +func (r *Repl) clearOnePhraseLeft() { + idx, ok := r.prevPhrasePos() + if ok { + newBuffer := append(r.buffer[0:idx], r.buffer[r.bufferPos:]...) + + newPos := idx + + r.prevDel = r.buffer[idx:r.bufferPos] + + _, y0 := r.cursorCoord(-1) + x1, y1 := r.cursorCoord(newPos) + + if r.bufferPos == r.bufferLen() && y0 == y1 && x1 > 0 && !r.overflow() { + r.bufferPos = newPos + r.buffer = newBuffer + r.syncCursor() + clearRowAfterCursor() + } else { + r.force(newBuffer, newPos) + } + } +} + +func (r *Repl) clearOnePhraseRight() { + idx, ok := r.nextPhrasePos() + if ok { + newBuffer := make([]byte, 0) + newBuffer = append(newBuffer, r.buffer[0:r.bufferPos]...) + newBuffer = append(newBuffer, r.buffer[idx:]...) + + newPos := r.bufferPos + + r.prevDel = r.buffer[r.bufferPos:idx] + + r.force(newBuffer, newPos) + } +} + +func (r *Repl) cleanAndAddToBuffer(msg []byte) { + // remove bad chars + // XXX: what about unicode? + filtered := make([]byte, 0) + + for _, c := range msg { + if c == '\t' { + filtered = append(filtered, ' ') + } else if c >= 32 && c < 127 { + filtered = append(filtered, c) + } + } + + r.addBytesToBuffer(filtered) +} + +func (r *Repl) insertPrevDel() { + r.addBytesToBuffer(r.prevDel) +} + +func (r *Repl) updatePromptRow(row int) { + if row >= r.getHeight() { + row = r.getHeight() - 1 + } else if row < 0 { + row = 0 + } + + r.promptRow = row + + r.log("prompt row %d/%d\n", r.promptRow, r.innerHeight()-1) +} + +func (r *Repl) writeByte(b byte) { + if b == '\n' { + r.newLine() + } else { + // should be a printable character + fmt.Fprintf(os.Stdout, "%c", b) + } +} + +func (r *Repl) newLine() { + fmt.Fprintf(os.Stdout, "\n\r") + + // every newLine means the status line is pushed below +} + +func (r *Repl) CwdStatusWidget() string { + cwd, err := os.Getwd() + if err != nil { + cwd = "" + } + + return cwd +} + +func (r *Repl) VisStatusWidget() string { + vis := "All" + + if r.viewEnd < 0 { + r.viewEnd = r.bufferLen() + } + + if r.viewEnd < r.bufferLen() && r.viewStart == 0 { + vis = "Start" + } else if r.viewEnd == r.bufferLen() && r.viewStart > 0 { + vis = "End" + } else if r.viewEnd < r.bufferLen() && r.viewStart > 0 { + vis = fmt.Sprintf("%d", int(float64(r.bufferPos)/float64(r.bufferLen())*100)) + "%" + } + + return vis +} + +// one left aligned and one right aligned +func (r *Repl) statusFields() (string, string) { + var leftWidget, rightWidget string + if r.StatusWidgets == nil || r.StatusWidgets.Left == nil { + leftWidget = r.CwdStatusWidget() + } else { + leftWidget = r.StatusWidgets.Left(r) + } + + if r.StatusWidgets == nil || r.StatusWidgets.Right == nil { + rightWidget = r.VisStatusWidget() + } else { + rightWidget = r.StatusWidgets.Right(r) + } + + return leftWidget, rightWidget +} + +func (r *Repl) statusVisible() bool { + if r.getWidth() < 10 { + return false + } else { + return true + } +} + +func (r *Repl) clearStatus() { + if r.statusVisible() { + moveCursorTo(0, r.getHeight()-1) + + clearRow() + + r.syncCursor() + } +} + +func (r *Repl) filterStatus() string { + tot := 0 + cur := -1 + for i := len(r.history) - 1; i >= 0; i-- { + entry := r.history[i] + if r.filterMatches(entry) { + if i == r.historyIdx { + cur = tot + } + + tot += 1 + } + } + + if tot == 0 { + return "No matches" + } else if cur != -1 { + return fmt.Sprintf("%d/%d matches", cur+1, tot) + } else { + return fmt.Sprintf("%d matches", tot) + } +} + +func (r *Repl) writeStatus() { + if !r.statusVisible() { + r.syncCursor() + return + } + + r.boundPromptRow() + + moveCursorTo(0, r.getHeight()-1) + + w := r.getWidth() + if r.searchActive() { + pref := "Reverse-search: " + fmt.Print(pref) + fmt.Print(string(r.filter)) // cursor stays here + + // print some status about the matches + if len(r.filter) > 0 && w > len(r.filter)+len(pref)+10 { + info := r.filterStatus() + + for i := 0; i < w-len(info)-len(pref)-len(r.filter); i++ { + fmt.Print(" ") + } + + fmt.Print(info) + + moveToCol(len(pref) + len(r.filter)) + } + } else { + left, right := r.statusFields() + if len(right) > w { + right = right[:w] + } + + leftWidth := w - len(right) + if leftWidth < 0 { + leftWidth = 0 + } + if len(left) > leftWidth { + left = left[:leftWidth] + } + + // Start highlighting + highlight() + fmt.Print(left) + + // Re-highlight in case a custom status widget blew up the colors + highlight() + for i := 0; i < w-len(left)-len(right); i++ { + fmt.Print(" ") + } + + // Re-highlight in case a custom status widget blew up the colors + highlight() + fmt.Print(right) + + // end highlighting + resetDecorations() + + r.syncCursor() + } +} + +// use a simple match criterium now, could be improved +func (r *Repl) filterMatches(bs []byte) bool { + return strings.Contains(string(bs), string(r.filter)) +} + +func (r *Repl) updateSearchResult() { + if r.filter == nil || len(r.history) == 0 || len(r.filter) == 0 { + return + } + + // prefer currently selected entry + if r.historyIdx != -1 { + if r.filterMatches(r.buffer) { + return + } + } + + for i := len(r.history) - 1; i >= 0; i-- { + if r.filterMatches(r.history[i]) { + r.useHistoryEntry(i) + return + } + } +} + +/////////////////// +// exported methods +/////////////////// + +// Start the REPL loop. +// +// Loop sets the terminal to raw mode, so any further calls to fmt.Print or similar, might not behave as expected and can garble your REPL. +func (r *Repl) Loop() error { + // the terminal needs to be in raw mode, so we can intercept the control sequences + // (the default canonical mode isn't good enough for repl's) + if err := r.MakeRaw(); err != nil { + return err + } + + r.reader.start() + + r.notifySizeChange() + + r.printPrompt() + + queryCursorPos() // get initial prompt position + + // loop forever + for { + r.reader.read() + + bts := <-r.reader.bytes + + r.dispatch(bts) + } +} + +// Exit the REPL program cleanly. Performs the following steps: +// 1. cleans the screen +// 2. returns the cursor to the appropriate position +// 3. unsets terminal raw mode +// +// Important: use this method instead of os.Exit. +func (r *Repl) Quit() { + r.quit() +} + +// Unset the raw mode in case you want to run a curses-like command inside your REPL session (e.g. vi or top). Remember to call MakeRaw after the command finishes. +func (r *Repl) UnmakeRaw() { + if r.onEnd != nil { + r.onEnd() + } + + r.onEnd = nil +} + +// Explicitely set the terminal back to raw mode after a call to UnmakeRaw. +func (r *Repl) MakeRaw() error { + // we need the term package as a platform independent way of setting the connected terminal emulator to raw mode + fd := int(os.Stdin.Fd()) + + oldState, err := term.MakeRaw(fd) + if err != nil { + return err + } + + r.onEnd = func() { + term.Restore(fd, oldState) + } + + return nil +} + +func (r *Repl) ReadLine(echo bool) string { + buffer := make([]byte, 0) + + for { + r.reader.read() + + bts := <-r.reader.bytes + + // a mini version of dispatch + if len(bts) == 1 && bts[0] == 13 { + if echo { + fmt.Print("\n\r") + } + break + } else { + for _, b := range bts { + if b == 27 { + break + } else if b >= 32 { + if echo { + fmt.Print(string([]byte{b})) + } + + buffer = append(buffer, b) + } + } + } + } + + return string(buffer) +} diff --git a/tools/dawgrun/pkg/go-repl/repl_popover_test.go b/tools/dawgrun/pkg/go-repl/repl_popover_test.go new file mode 100644 index 0000000..6c3fdac --- /dev/null +++ b/tools/dawgrun/pkg/go-repl/repl_popover_test.go @@ -0,0 +1,99 @@ +package repl + +import "testing" + +func TestCompletionPopoverMode(t *testing.T) { + r := &Repl{} + + t.Setenv("REPL_COMPLETION_POPOVER_MODE", "") + t.Setenv("ZELLIJ", "") + if mode := r.completionPopoverMode(); mode != "overlay" { + t.Fatalf("expected overlay mode by default, got %q", mode) + } + + t.Setenv("ZELLIJ", "1") + if mode := r.completionPopoverMode(); mode != "overlay" { + t.Fatalf("expected overlay mode in zellij, got %q", mode) + } + + t.Setenv("REPL_COMPLETION_POPOVER_MODE", "insert") + if mode := r.completionPopoverMode(); mode != "insert" { + t.Fatalf("expected insert mode override, got %q", mode) + } + + t.Setenv("REPL_COMPLETION_POPOVER_MODE", "overlay") + if mode := r.completionPopoverMode(); mode != "overlay" { + t.Fatalf("expected overlay mode override, got %q", mode) + } +} + +func TestClampCompletionLines(t *testing.T) { + testCases := []struct { + name string + lines []string + max int + expect []string + }{ + {name: "no rows", lines: []string{"a", "b"}, max: 0, expect: nil}, + {name: "fits", lines: []string{"a", "b"}, max: 2, expect: []string{"a", "b"}}, + {name: "single row summary", lines: []string{"a", "b", "c"}, max: 1, expect: []string{"3 matches"}}, + {name: "truncated with remainder", lines: []string{"a", "b", "c", "d"}, max: 2, expect: []string{"a", "... 3 more"}}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := clampCompletionLines(tc.lines, tc.max) + if len(got) != len(tc.expect) { + t.Fatalf("expected %d lines, got %d (%v)", len(tc.expect), len(got), got) + } + + for i := range tc.expect { + if got[i] != tc.expect[i] { + t.Fatalf("expected line[%d]=%q, got %q", i, tc.expect[i], got[i]) + } + } + }) + } +} + +func TestIsCursorQueryResponse(t *testing.T) { + testCases := []struct { + name string + bytes []byte + expect bool + }{ + {name: "standard response", bytes: []byte{27, 91, 50, 48, 59, 49, 82}, expect: true}, + {name: "response with prefix text", bytes: []byte{'x', 'y', 27, 91, 49, 59, 49, 82}, expect: true}, + {name: "arrow left", bytes: []byte{27, 91, 68}, expect: false}, + {name: "regular character", bytes: []byte{'q'}, expect: false}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if got := isCursorQueryResponse(tc.bytes); got != tc.expect { + t.Fatalf("expected %v, got %v", tc.expect, got) + } + }) + } +} + +func TestTruncateCompletionLine(t *testing.T) { + testCases := []struct { + name string + line string + width int + expect string + }{ + {name: "fits", line: "hello", width: 8, expect: "hello"}, + {name: "small width", line: "hello", width: 2, expect: "he"}, + {name: "ellipsis", line: "completion-candidate", width: 10, expect: "complet..."}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if got := truncateCompletionLine(tc.line, tc.width); got != tc.expect { + t.Fatalf("expected %q, got %q", tc.expect, got) + } + }) + } +} diff --git a/tools/dawgrun/pkg/go-repl/stdinreader.go b/tools/dawgrun/pkg/go-repl/stdinreader.go new file mode 100644 index 0000000..c481c07 --- /dev/null +++ b/tools/dawgrun/pkg/go-repl/stdinreader.go @@ -0,0 +1,103 @@ +package repl + +import ( + "bufio" + "errors" + "io" + "os" + "sync" + "time" +) + +// This is a cut-off time for grouping auto-generated escape sequences. +const MACHINE_INTERVAL = time.Millisecond + +// _StdinReader collects inputs and keeps sequences of auto-generated bytes together as a group (eg. ansi escape sequences) +type _StdinReader struct { + reader *bufio.Reader + lastTime time.Time + buffer []byte + lock *sync.Mutex + + bytes chan []byte +} + +func newStdinReader() *_StdinReader { + return &_StdinReader{ + reader: nil, + lastTime: time.Time{}, + buffer: make([]byte, 0), + lock: &sync.Mutex{}, + + bytes: make(chan []byte), + } +} + +func (r *_StdinReader) start() { + go func() { + ticker := time.NewTicker(MACHINE_INTERVAL) + defer ticker.Stop() + + for { + <-ticker.C + + r.lock.Lock() + + if len(r.buffer) > 0 { + if time.Now().After(r.lastTime.Add(MACHINE_INTERVAL)) { + msg := r.buffer + r.buffer = make([]byte, 0) + r.lock.Unlock() + + r.bytes <- msg + continue + } + } + + r.lock.Unlock() + } + }() +} + +func (r *_StdinReader) read() { + r.lock.Lock() + if r.reader != nil { + r.lock.Unlock() + return + } + + r.reader = bufio.NewReader(os.Stdin) + r.lastTime = time.Now() + r.lock.Unlock() + + go func() { + for { + b, err := r.reader.ReadByte() + if err != nil { + if errors.Is(err, io.EOF) { + return + } + + panic(err) + } + + stopNow := false + r.lock.Lock() + if b == 13 && time.Now().After(r.lastTime.Add(MACHINE_INTERVAL)) { + // it is unlikely that a carriage return followed by some text is pasted into the terminal, so we can use this as a queu to quit + stopNow = true + } + + r.lastTime = time.Now() + r.buffer = append(r.buffer, b) + r.lock.Unlock() + + if stopNow { + r.lock.Lock() + r.reader = nil + r.lock.Unlock() + return + } + } + }() +} diff --git a/tools/dawgrun/pkg/stubs/kindmapper.go b/tools/dawgrun/pkg/stubs/kindmapper.go new file mode 100644 index 0000000..6dd4361 --- /dev/null +++ b/tools/dawgrun/pkg/stubs/kindmapper.go @@ -0,0 +1,107 @@ +// Package stubs has various bits and bobs to stub out internal behaviors +package stubs + +import ( + "context" + "errors" + "fmt" + + "github.com/specterops/dawgs/cypher/models/pgsql" + "github.com/specterops/dawgs/graph" +) + +var ( + ErrNoSuchKind = errors.New("no such kind") + ErrNoSuchKindID = errors.New("no such kind with ID") +) + +type KindMap map[int16]graph.Kind + +func (km KindMap) Invert() map[graph.Kind]int16 { + inverse := make(map[graph.Kind]int16) + for kindID, kind := range km { + inverse[kind] = kindID + } + + return inverse +} + +type DumbKindMapper struct { + idToKind KindMap + kindToID map[graph.Kind]int16 + lastID int16 +} + +var _ pgsql.KindMapper = (*DumbKindMapper)(nil) + +func EmptyMapper() *DumbKindMapper { + return &DumbKindMapper{ + idToKind: make(KindMap), + kindToID: make(map[graph.Kind]int16), + lastID: -1, + } +} + +func MapperFromKindMap(kindMap KindMap) *DumbKindMapper { + idToKind := make(KindMap, len(kindMap)) + lastID := int16(-1) + for id, kind := range kindMap { + idToKind[id] = kind + if id > lastID { + lastID = id + } + } + + return &DumbKindMapper{ + idToKind: idToKind, + kindToID: idToKind.Invert(), + lastID: lastID, + } +} + +func (k *DumbKindMapper) MapKinds(ctx context.Context, kinds graph.Kinds) ([]int16, error) { + kindIDs := make([]int16, len(kinds)) + for idx, kind := range kinds { + if mappedKindID, ok := k.kindToID[kind]; !ok { + return nil, fmt.Errorf("%w: %v", ErrNoSuchKind, kind) + } else { + kindIDs[idx] = mappedKindID + } + } + return kindIDs, nil +} + +// AssertKinds tries to return IDs of `graph.Kind`s that are already known, inserting any kinds not known +// into the schema. +func (k *DumbKindMapper) AssertKinds(ctx context.Context, kinds graph.Kinds) ([]int16, error) { + kindIDs := make([]int16, 0) + for _, kind := range kinds { + if mappedKindID, ok := k.kindToID[kind]; !ok { + newID := k.lastID + 1 + k.lastID += 1 + k.kindToID[kind] = newID + k.idToKind[newID] = kind + kindIDs = append(kindIDs, newID) + } else { + kindIDs = append(kindIDs, mappedKindID) + } + } + + return kindIDs, nil +} + +func (k *DumbKindMapper) GetKindByID(id int16) (graph.Kind, error) { + if kind, ok := k.idToKind[id]; !ok { + return nil, fmt.Errorf("%w: %d", ErrNoSuchKindID, id) + } else { + return kind, nil + } +} + +func (k *DumbKindMapper) GetIDByKind(kind graph.Kind) (int16, error) { + if kindID, ok := k.kindToID[kind]; !ok { + return -1, fmt.Errorf("%w: %v", ErrNoSuchKind, kind) + } else { + return kindID, nil + } +}