diff --git a/.gemini/settings.json b/.gemini/settings.json new file mode 100644 index 0000000..b458a9e --- /dev/null +++ b/.gemini/settings.json @@ -0,0 +1,11 @@ +{ + "general": { + "sessionRetention": { + "enabled": true + }, + "enablePromptCompletion": true + }, + "experimental": { + "plan": true + } +} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 796525b..85afc54 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: core --version - name: Generate code - run: go generate ./pkg/updater/... + run: go generate ./internal/cmd/updater/... - name: Run QA # Skip lint until golangci-lint supports Go 1.25 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 636eca1..dea41fe 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -33,7 +33,7 @@ jobs: core --version - name: Generate code - run: go generate ./pkg/updater/... + run: go generate ./internal/cmd/updater/... - name: Run coverage run: core go cov diff --git a/.github/workflows/dev-release.yml b/.github/workflows/dev-release.yml index 16c22f6..8657737 100644 --- a/.github/workflows/dev-release.yml +++ b/.github/workflows/dev-release.yml @@ -30,7 +30,7 @@ jobs: core --version - name: Generate code - run: go generate ./pkg/updater/... + run: go generate ./internal/cmd/updater/... - name: Build all targets run: core build --targets=linux/amd64,linux/arm64,darwin/amd64,darwin/arm64,windows/amd64,windows/arm64 --ci diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1d98ef2..61b5c53 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ jobs: core --version - name: Generate code - run: go generate ./pkg/updater/... + run: go generate ./internal/cmd/updater/... - name: Build all targets run: core build --targets=linux/amd64,linux/arm64,darwin/amd64,darwin/arm64,windows/amd64,windows/arm64 --ci diff --git a/go.mod b/go.mod index 3847aef..242bca5 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.25.5 require ( github.com/Snider/Borg v0.1.0 github.com/getkin/kin-openapi v0.133.0 + github.com/host-uk/core-gui v0.0.0-20260131214111-6e2460834a87 github.com/leaanthony/debme v1.2.1 github.com/leaanthony/gosod v1.0.4 github.com/minio/selfupdate v0.6.0 @@ -23,17 +24,17 @@ require ( require ( aead.dev/minisign v0.3.0 // indirect cloud.google.com/go v0.123.0 // indirect - dario.cat/mergo v1.0.0 // indirect + dario.cat/mergo v1.0.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.3.0 // indirect github.com/TwiN/go-color v1.4.1 // indirect - github.com/cloudflare/circl v1.6.1 // indirect - github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/cloudflare/circl v1.6.3 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.6.2 // indirect - github.com/go-git/go-git/v5 v5.16.3 // indirect + github.com/go-git/go-billy/v5 v5.7.0 // indirect + github.com/go-git/go-git/v5 v5.16.4 // indirect github.com/go-openapi/jsonpointer v0.22.4 // indirect github.com/go-openapi/swag/jsonname v0.25.4 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect @@ -41,16 +42,17 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/kevinburke/ssh_config v1.4.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect - github.com/pjbgf/sha1cd v0.3.2 // indirect + github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect - github.com/skeema/knownhosts v1.3.1 // indirect + github.com/sergi/go-diff v1.4.0 // indirect + github.com/skeema/knownhosts v1.3.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.2.0 // indirect diff --git a/go.sum b/go.sum index 19ff715..f17621d 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,8 @@ aead.dev/minisign v0.3.0 h1:8Xafzy5PEVZqYDNP60yJHARlW1eOQtsKNp/Ph2c0vRA= aead.dev/minisign v0.3.0/go.mod h1:NLvG3Uoq3skkRMDuc3YHpWUTMTrSExqm+Ij73W13F6Y= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= @@ -18,11 +18,11 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= -github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= -github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -37,12 +37,12 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= -github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM= +github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.16.3 h1:Z8BtvxZ09bYm/yYNgPKCzgWtaRqDTgIKRgIRHBfU6Z8= -github.com/go-git/go-git/v5 v5.16.3/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= +github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= @@ -59,14 +59,18 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/host-uk/core-gui v0.0.0-20260131214111-6e2460834a87 h1:gCdRVNxL1GpKhiYhtqJ60xm2ML3zU/UbYR9lHzlAWb8= +github.com/host-uk/core-gui v0.0.0-20260131214111-6e2460834a87/go.mod h1:yOBnW4of0/82O6GSxFl2Pxepq9yTlJg2pLVwaU9cWHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= -github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= +github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -102,8 +106,8 @@ github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= -github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= -github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= +github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= 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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -112,11 +116,11 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= -github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= -github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg= +github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -138,8 +142,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/wI2L/jsondiff v0.7.0 h1:1lH1G37GhBPqCfp/lrs91rf/2j3DktX6qYAKZkLuCQQ= github.com/wI2L/jsondiff v0.7.0/go.mod h1:KAEIojdQq66oJiHhDyQez2x+sRit0vIzC9KeK0yizxM= github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= diff --git a/pkg/ai/cmd_ai.go b/internal/cmd/ai/cmd_ai.go similarity index 100% rename from pkg/ai/cmd_ai.go rename to internal/cmd/ai/cmd_ai.go diff --git a/pkg/ai/cmd_commands.go b/internal/cmd/ai/cmd_commands.go similarity index 100% rename from pkg/ai/cmd_commands.go rename to internal/cmd/ai/cmd_commands.go diff --git a/pkg/ai/cmd_git.go b/internal/cmd/ai/cmd_git.go similarity index 100% rename from pkg/ai/cmd_git.go rename to internal/cmd/ai/cmd_git.go diff --git a/pkg/ai/cmd_tasks.go b/internal/cmd/ai/cmd_tasks.go similarity index 100% rename from pkg/ai/cmd_tasks.go rename to internal/cmd/ai/cmd_tasks.go diff --git a/pkg/ai/cmd_updates.go b/internal/cmd/ai/cmd_updates.go similarity index 100% rename from pkg/ai/cmd_updates.go rename to internal/cmd/ai/cmd_updates.go diff --git a/pkg/ci/cmd_changelog.go b/internal/cmd/ci/cmd_changelog.go similarity index 100% rename from pkg/ci/cmd_changelog.go rename to internal/cmd/ci/cmd_changelog.go diff --git a/pkg/ci/cmd_ci.go b/internal/cmd/ci/cmd_ci.go similarity index 100% rename from pkg/ci/cmd_ci.go rename to internal/cmd/ci/cmd_ci.go diff --git a/pkg/ci/cmd_commands.go b/internal/cmd/ci/cmd_commands.go similarity index 100% rename from pkg/ci/cmd_commands.go rename to internal/cmd/ci/cmd_commands.go diff --git a/pkg/ci/cmd_init.go b/internal/cmd/ci/cmd_init.go similarity index 100% rename from pkg/ci/cmd_init.go rename to internal/cmd/ci/cmd_init.go diff --git a/pkg/ci/cmd_publish.go b/internal/cmd/ci/cmd_publish.go similarity index 100% rename from pkg/ci/cmd_publish.go rename to internal/cmd/ci/cmd_publish.go diff --git a/pkg/ci/cmd_version.go b/internal/cmd/ci/cmd_version.go similarity index 100% rename from pkg/ci/cmd_version.go rename to internal/cmd/ci/cmd_version.go diff --git a/pkg/dev/cmd_api.go b/internal/cmd/dev/cmd_api.go similarity index 100% rename from pkg/dev/cmd_api.go rename to internal/cmd/dev/cmd_api.go diff --git a/pkg/dev/cmd_apply.go b/internal/cmd/dev/cmd_apply.go similarity index 100% rename from pkg/dev/cmd_apply.go rename to internal/cmd/dev/cmd_apply.go diff --git a/pkg/dev/cmd_bundles.go b/internal/cmd/dev/cmd_bundles.go similarity index 100% rename from pkg/dev/cmd_bundles.go rename to internal/cmd/dev/cmd_bundles.go diff --git a/pkg/dev/cmd_ci.go b/internal/cmd/dev/cmd_ci.go similarity index 100% rename from pkg/dev/cmd_ci.go rename to internal/cmd/dev/cmd_ci.go diff --git a/pkg/dev/cmd_commit.go b/internal/cmd/dev/cmd_commit.go similarity index 100% rename from pkg/dev/cmd_commit.go rename to internal/cmd/dev/cmd_commit.go diff --git a/pkg/dev/cmd_dev.go b/internal/cmd/dev/cmd_dev.go similarity index 100% rename from pkg/dev/cmd_dev.go rename to internal/cmd/dev/cmd_dev.go diff --git a/pkg/dev/cmd_file_sync.go b/internal/cmd/dev/cmd_file_sync.go similarity index 100% rename from pkg/dev/cmd_file_sync.go rename to internal/cmd/dev/cmd_file_sync.go diff --git a/pkg/dev/cmd_health.go b/internal/cmd/dev/cmd_health.go similarity index 100% rename from pkg/dev/cmd_health.go rename to internal/cmd/dev/cmd_health.go diff --git a/pkg/dev/cmd_impact.go b/internal/cmd/dev/cmd_impact.go similarity index 100% rename from pkg/dev/cmd_impact.go rename to internal/cmd/dev/cmd_impact.go diff --git a/pkg/dev/cmd_issues.go b/internal/cmd/dev/cmd_issues.go similarity index 100% rename from pkg/dev/cmd_issues.go rename to internal/cmd/dev/cmd_issues.go diff --git a/pkg/dev/cmd_pull.go b/internal/cmd/dev/cmd_pull.go similarity index 100% rename from pkg/dev/cmd_pull.go rename to internal/cmd/dev/cmd_pull.go diff --git a/pkg/dev/cmd_push.go b/internal/cmd/dev/cmd_push.go similarity index 100% rename from pkg/dev/cmd_push.go rename to internal/cmd/dev/cmd_push.go diff --git a/pkg/dev/cmd_reviews.go b/internal/cmd/dev/cmd_reviews.go similarity index 100% rename from pkg/dev/cmd_reviews.go rename to internal/cmd/dev/cmd_reviews.go diff --git a/pkg/dev/cmd_sync.go b/internal/cmd/dev/cmd_sync.go similarity index 100% rename from pkg/dev/cmd_sync.go rename to internal/cmd/dev/cmd_sync.go diff --git a/pkg/dev/cmd_vm.go b/internal/cmd/dev/cmd_vm.go similarity index 100% rename from pkg/dev/cmd_vm.go rename to internal/cmd/dev/cmd_vm.go diff --git a/pkg/dev/cmd_work.go b/internal/cmd/dev/cmd_work.go similarity index 100% rename from pkg/dev/cmd_work.go rename to internal/cmd/dev/cmd_work.go diff --git a/pkg/dev/cmd_workflow.go b/internal/cmd/dev/cmd_workflow.go similarity index 100% rename from pkg/dev/cmd_workflow.go rename to internal/cmd/dev/cmd_workflow.go diff --git a/pkg/dev/cmd_workflow_test.go b/internal/cmd/dev/cmd_workflow_test.go similarity index 100% rename from pkg/dev/cmd_workflow_test.go rename to internal/cmd/dev/cmd_workflow_test.go diff --git a/pkg/dev/registry.go b/internal/cmd/dev/registry.go similarity index 97% rename from pkg/dev/registry.go rename to internal/cmd/dev/registry.go index d51c0af..8ead92a 100644 --- a/pkg/dev/registry.go +++ b/internal/cmd/dev/registry.go @@ -5,10 +5,10 @@ import ( "path/filepath" "strings" + "github.com/host-uk/core/internal/cmd/workspace" "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/repos" - "github.com/host-uk/core/pkg/workspace" ) // loadRegistryWithConfig loads the registry and applies workspace configuration. diff --git a/pkg/dev/service.go b/internal/cmd/dev/service.go similarity index 100% rename from pkg/dev/service.go rename to internal/cmd/dev/service.go diff --git a/pkg/docs/cmd_commands.go b/internal/cmd/docs/cmd_commands.go similarity index 100% rename from pkg/docs/cmd_commands.go rename to internal/cmd/docs/cmd_commands.go diff --git a/pkg/docs/cmd_docs.go b/internal/cmd/docs/cmd_docs.go similarity index 100% rename from pkg/docs/cmd_docs.go rename to internal/cmd/docs/cmd_docs.go diff --git a/pkg/docs/cmd_list.go b/internal/cmd/docs/cmd_list.go similarity index 100% rename from pkg/docs/cmd_list.go rename to internal/cmd/docs/cmd_list.go diff --git a/pkg/docs/cmd_scan.go b/internal/cmd/docs/cmd_scan.go similarity index 98% rename from pkg/docs/cmd_scan.go rename to internal/cmd/docs/cmd_scan.go index 4300d03..8257c94 100644 --- a/pkg/docs/cmd_scan.go +++ b/internal/cmd/docs/cmd_scan.go @@ -6,10 +6,10 @@ import ( "path/filepath" "strings" + "github.com/host-uk/core/internal/cmd/workspace" "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/repos" - "github.com/host-uk/core/pkg/workspace" ) // RepoDocInfo holds documentation info for a repo diff --git a/pkg/docs/cmd_sync.go b/internal/cmd/docs/cmd_sync.go similarity index 100% rename from pkg/docs/cmd_sync.go rename to internal/cmd/docs/cmd_sync.go diff --git a/pkg/doctor/cmd_checks.go b/internal/cmd/doctor/cmd_checks.go similarity index 100% rename from pkg/doctor/cmd_checks.go rename to internal/cmd/doctor/cmd_checks.go diff --git a/pkg/doctor/cmd_commands.go b/internal/cmd/doctor/cmd_commands.go similarity index 100% rename from pkg/doctor/cmd_commands.go rename to internal/cmd/doctor/cmd_commands.go diff --git a/pkg/doctor/cmd_doctor.go b/internal/cmd/doctor/cmd_doctor.go similarity index 100% rename from pkg/doctor/cmd_doctor.go rename to internal/cmd/doctor/cmd_doctor.go diff --git a/pkg/doctor/cmd_environment.go b/internal/cmd/doctor/cmd_environment.go similarity index 100% rename from pkg/doctor/cmd_environment.go rename to internal/cmd/doctor/cmd_environment.go diff --git a/pkg/doctor/cmd_install.go b/internal/cmd/doctor/cmd_install.go similarity index 100% rename from pkg/doctor/cmd_install.go rename to internal/cmd/doctor/cmd_install.go diff --git a/pkg/gitcmd/cmd_git.go b/internal/cmd/gitcmd/cmd_git.go similarity index 96% rename from pkg/gitcmd/cmd_git.go rename to internal/cmd/gitcmd/cmd_git.go index 7c6d369..32b203b 100644 --- a/pkg/gitcmd/cmd_git.go +++ b/internal/cmd/gitcmd/cmd_git.go @@ -13,8 +13,8 @@ package gitcmd import ( + "github.com/host-uk/core/internal/cmd/dev" "github.com/host-uk/core/pkg/cli" - "github.com/host-uk/core/pkg/dev" "github.com/host-uk/core/pkg/i18n" ) diff --git a/pkg/go/cmd_commands.go b/internal/cmd/go/cmd_commands.go similarity index 100% rename from pkg/go/cmd_commands.go rename to internal/cmd/go/cmd_commands.go diff --git a/pkg/go/cmd_format.go b/internal/cmd/go/cmd_format.go similarity index 100% rename from pkg/go/cmd_format.go rename to internal/cmd/go/cmd_format.go diff --git a/pkg/go/cmd_go.go b/internal/cmd/go/cmd_go.go similarity index 100% rename from pkg/go/cmd_go.go rename to internal/cmd/go/cmd_go.go diff --git a/pkg/go/cmd_gotest.go b/internal/cmd/go/cmd_gotest.go similarity index 100% rename from pkg/go/cmd_gotest.go rename to internal/cmd/go/cmd_gotest.go diff --git a/pkg/go/cmd_qa.go b/internal/cmd/go/cmd_qa.go similarity index 99% rename from pkg/go/cmd_qa.go rename to internal/cmd/go/cmd_qa.go index 51af1b8..910852f 100644 --- a/pkg/go/cmd_qa.go +++ b/internal/cmd/go/cmd_qa.go @@ -9,9 +9,9 @@ import ( "strings" "time" + "github.com/host-uk/core/internal/cmd/qa" "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" - "github.com/host-uk/core/pkg/qa" ) // QA command flags - comprehensive options for all agents diff --git a/pkg/go/cmd_tools.go b/internal/cmd/go/cmd_tools.go similarity index 100% rename from pkg/go/cmd_tools.go rename to internal/cmd/go/cmd_tools.go diff --git a/pkg/monitor/cmd_commands.go b/internal/cmd/monitor/cmd_commands.go similarity index 100% rename from pkg/monitor/cmd_commands.go rename to internal/cmd/monitor/cmd_commands.go diff --git a/pkg/monitor/cmd_monitor.go b/internal/cmd/monitor/cmd_monitor.go similarity index 100% rename from pkg/monitor/cmd_monitor.go rename to internal/cmd/monitor/cmd_monitor.go diff --git a/pkg/php/cmd.go b/internal/cmd/php/cmd.go similarity index 98% rename from pkg/php/cmd.go rename to internal/cmd/php/cmd.go index f2cd2d8..80091ea 100644 --- a/pkg/php/cmd.go +++ b/internal/cmd/php/cmd.go @@ -4,9 +4,9 @@ import ( "os" "path/filepath" + "github.com/host-uk/core/internal/cmd/workspace" "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" - "github.com/host-uk/core/pkg/workspace" "github.com/spf13/cobra" ) diff --git a/pkg/php/cmd_build.go b/internal/cmd/php/cmd_build.go similarity index 100% rename from pkg/php/cmd_build.go rename to internal/cmd/php/cmd_build.go diff --git a/pkg/php/cmd_ci.go b/internal/cmd/php/cmd_ci.go similarity index 100% rename from pkg/php/cmd_ci.go rename to internal/cmd/php/cmd_ci.go diff --git a/pkg/php/cmd_commands.go b/internal/cmd/php/cmd_commands.go similarity index 100% rename from pkg/php/cmd_commands.go rename to internal/cmd/php/cmd_commands.go diff --git a/pkg/php/cmd_deploy.go b/internal/cmd/php/cmd_deploy.go similarity index 100% rename from pkg/php/cmd_deploy.go rename to internal/cmd/php/cmd_deploy.go diff --git a/pkg/php/cmd_dev.go b/internal/cmd/php/cmd_dev.go similarity index 100% rename from pkg/php/cmd_dev.go rename to internal/cmd/php/cmd_dev.go diff --git a/pkg/php/cmd_packages.go b/internal/cmd/php/cmd_packages.go similarity index 100% rename from pkg/php/cmd_packages.go rename to internal/cmd/php/cmd_packages.go diff --git a/pkg/php/cmd_qa_runner.go b/internal/cmd/php/cmd_qa_runner.go similarity index 100% rename from pkg/php/cmd_qa_runner.go rename to internal/cmd/php/cmd_qa_runner.go diff --git a/pkg/php/cmd_quality.go b/internal/cmd/php/cmd_quality.go similarity index 100% rename from pkg/php/cmd_quality.go rename to internal/cmd/php/cmd_quality.go diff --git a/pkg/php/container.go b/internal/cmd/php/container.go similarity index 100% rename from pkg/php/container.go rename to internal/cmd/php/container.go diff --git a/pkg/php/container_test.go b/internal/cmd/php/container_test.go similarity index 100% rename from pkg/php/container_test.go rename to internal/cmd/php/container_test.go diff --git a/pkg/php/coolify.go b/internal/cmd/php/coolify.go similarity index 100% rename from pkg/php/coolify.go rename to internal/cmd/php/coolify.go diff --git a/pkg/php/coolify_test.go b/internal/cmd/php/coolify_test.go similarity index 100% rename from pkg/php/coolify_test.go rename to internal/cmd/php/coolify_test.go diff --git a/pkg/php/deploy.go b/internal/cmd/php/deploy.go similarity index 100% rename from pkg/php/deploy.go rename to internal/cmd/php/deploy.go diff --git a/pkg/php/deploy_internal_test.go b/internal/cmd/php/deploy_internal_test.go similarity index 100% rename from pkg/php/deploy_internal_test.go rename to internal/cmd/php/deploy_internal_test.go diff --git a/pkg/php/deploy_test.go b/internal/cmd/php/deploy_test.go similarity index 100% rename from pkg/php/deploy_test.go rename to internal/cmd/php/deploy_test.go diff --git a/pkg/php/detect.go b/internal/cmd/php/detect.go similarity index 100% rename from pkg/php/detect.go rename to internal/cmd/php/detect.go diff --git a/pkg/php/detect_test.go b/internal/cmd/php/detect_test.go similarity index 100% rename from pkg/php/detect_test.go rename to internal/cmd/php/detect_test.go diff --git a/pkg/php/dockerfile.go b/internal/cmd/php/dockerfile.go similarity index 100% rename from pkg/php/dockerfile.go rename to internal/cmd/php/dockerfile.go diff --git a/pkg/php/dockerfile_test.go b/internal/cmd/php/dockerfile_test.go similarity index 100% rename from pkg/php/dockerfile_test.go rename to internal/cmd/php/dockerfile_test.go diff --git a/pkg/php/i18n.go b/internal/cmd/php/i18n.go similarity index 100% rename from pkg/php/i18n.go rename to internal/cmd/php/i18n.go diff --git a/pkg/php/locales/en_GB.json b/internal/cmd/php/locales/en_GB.json similarity index 100% rename from pkg/php/locales/en_GB.json rename to internal/cmd/php/locales/en_GB.json diff --git a/pkg/php/packages.go b/internal/cmd/php/packages.go similarity index 100% rename from pkg/php/packages.go rename to internal/cmd/php/packages.go diff --git a/pkg/php/packages_test.go b/internal/cmd/php/packages_test.go similarity index 100% rename from pkg/php/packages_test.go rename to internal/cmd/php/packages_test.go diff --git a/pkg/php/php.go b/internal/cmd/php/php.go similarity index 100% rename from pkg/php/php.go rename to internal/cmd/php/php.go diff --git a/pkg/php/php_test.go b/internal/cmd/php/php_test.go similarity index 100% rename from pkg/php/php_test.go rename to internal/cmd/php/php_test.go diff --git a/pkg/php/quality.go b/internal/cmd/php/quality.go similarity index 100% rename from pkg/php/quality.go rename to internal/cmd/php/quality.go diff --git a/pkg/php/quality_extended_test.go b/internal/cmd/php/quality_extended_test.go similarity index 100% rename from pkg/php/quality_extended_test.go rename to internal/cmd/php/quality_extended_test.go diff --git a/pkg/php/quality_test.go b/internal/cmd/php/quality_test.go similarity index 100% rename from pkg/php/quality_test.go rename to internal/cmd/php/quality_test.go diff --git a/pkg/php/services.go b/internal/cmd/php/services.go similarity index 100% rename from pkg/php/services.go rename to internal/cmd/php/services.go diff --git a/pkg/php/services_extended_test.go b/internal/cmd/php/services_extended_test.go similarity index 100% rename from pkg/php/services_extended_test.go rename to internal/cmd/php/services_extended_test.go diff --git a/pkg/php/services_test.go b/internal/cmd/php/services_test.go similarity index 100% rename from pkg/php/services_test.go rename to internal/cmd/php/services_test.go diff --git a/pkg/php/services_unix.go b/internal/cmd/php/services_unix.go similarity index 100% rename from pkg/php/services_unix.go rename to internal/cmd/php/services_unix.go diff --git a/pkg/php/services_windows.go b/internal/cmd/php/services_windows.go similarity index 100% rename from pkg/php/services_windows.go rename to internal/cmd/php/services_windows.go diff --git a/pkg/php/ssl.go b/internal/cmd/php/ssl.go similarity index 100% rename from pkg/php/ssl.go rename to internal/cmd/php/ssl.go diff --git a/pkg/php/ssl_extended_test.go b/internal/cmd/php/ssl_extended_test.go similarity index 100% rename from pkg/php/ssl_extended_test.go rename to internal/cmd/php/ssl_extended_test.go diff --git a/pkg/php/ssl_test.go b/internal/cmd/php/ssl_test.go similarity index 100% rename from pkg/php/ssl_test.go rename to internal/cmd/php/ssl_test.go diff --git a/pkg/php/testing.go b/internal/cmd/php/testing.go similarity index 100% rename from pkg/php/testing.go rename to internal/cmd/php/testing.go diff --git a/pkg/php/testing_test.go b/internal/cmd/php/testing_test.go similarity index 100% rename from pkg/php/testing_test.go rename to internal/cmd/php/testing_test.go diff --git a/pkg/pkgcmd/cmd_commands.go b/internal/cmd/pkgcmd/cmd_commands.go similarity index 100% rename from pkg/pkgcmd/cmd_commands.go rename to internal/cmd/pkgcmd/cmd_commands.go diff --git a/pkg/pkgcmd/cmd_install.go b/internal/cmd/pkgcmd/cmd_install.go similarity index 100% rename from pkg/pkgcmd/cmd_install.go rename to internal/cmd/pkgcmd/cmd_install.go diff --git a/pkg/pkgcmd/cmd_manage.go b/internal/cmd/pkgcmd/cmd_manage.go similarity index 100% rename from pkg/pkgcmd/cmd_manage.go rename to internal/cmd/pkgcmd/cmd_manage.go diff --git a/pkg/pkgcmd/cmd_pkg.go b/internal/cmd/pkgcmd/cmd_pkg.go similarity index 100% rename from pkg/pkgcmd/cmd_pkg.go rename to internal/cmd/pkgcmd/cmd_pkg.go diff --git a/pkg/pkgcmd/cmd_search.go b/internal/cmd/pkgcmd/cmd_search.go similarity index 100% rename from pkg/pkgcmd/cmd_search.go rename to internal/cmd/pkgcmd/cmd_search.go diff --git a/pkg/qa/cmd_docblock.go b/internal/cmd/qa/cmd_docblock.go similarity index 100% rename from pkg/qa/cmd_docblock.go rename to internal/cmd/qa/cmd_docblock.go diff --git a/pkg/qa/cmd_health.go b/internal/cmd/qa/cmd_health.go similarity index 100% rename from pkg/qa/cmd_health.go rename to internal/cmd/qa/cmd_health.go diff --git a/pkg/qa/cmd_issues.go b/internal/cmd/qa/cmd_issues.go similarity index 100% rename from pkg/qa/cmd_issues.go rename to internal/cmd/qa/cmd_issues.go diff --git a/pkg/qa/cmd_qa.go b/internal/cmd/qa/cmd_qa.go similarity index 100% rename from pkg/qa/cmd_qa.go rename to internal/cmd/qa/cmd_qa.go diff --git a/pkg/qa/cmd_review.go b/internal/cmd/qa/cmd_review.go similarity index 100% rename from pkg/qa/cmd_review.go rename to internal/cmd/qa/cmd_review.go diff --git a/pkg/qa/cmd_watch.go b/internal/cmd/qa/cmd_watch.go similarity index 100% rename from pkg/qa/cmd_watch.go rename to internal/cmd/qa/cmd_watch.go diff --git a/pkg/sdk/cmd_commands.go b/internal/cmd/sdk/cmd_commands.go similarity index 100% rename from pkg/sdk/cmd_commands.go rename to internal/cmd/sdk/cmd_commands.go diff --git a/pkg/sdk/cmd_sdk.go b/internal/cmd/sdk/cmd_sdk.go similarity index 100% rename from pkg/sdk/cmd_sdk.go rename to internal/cmd/sdk/cmd_sdk.go diff --git a/pkg/sdk/detect.go b/internal/cmd/sdk/detect.go similarity index 100% rename from pkg/sdk/detect.go rename to internal/cmd/sdk/detect.go diff --git a/pkg/sdk/detect_test.go b/internal/cmd/sdk/detect_test.go similarity index 100% rename from pkg/sdk/detect_test.go rename to internal/cmd/sdk/detect_test.go diff --git a/pkg/sdk/diff.go b/internal/cmd/sdk/diff.go similarity index 100% rename from pkg/sdk/diff.go rename to internal/cmd/sdk/diff.go diff --git a/pkg/sdk/diff_test.go b/internal/cmd/sdk/diff_test.go similarity index 100% rename from pkg/sdk/diff_test.go rename to internal/cmd/sdk/diff_test.go diff --git a/pkg/sdk/generators/generator.go b/internal/cmd/sdk/generators/generator.go similarity index 100% rename from pkg/sdk/generators/generator.go rename to internal/cmd/sdk/generators/generator.go diff --git a/pkg/sdk/generators/go.go b/internal/cmd/sdk/generators/go.go similarity index 100% rename from pkg/sdk/generators/go.go rename to internal/cmd/sdk/generators/go.go diff --git a/pkg/sdk/generators/go_test.go b/internal/cmd/sdk/generators/go_test.go similarity index 100% rename from pkg/sdk/generators/go_test.go rename to internal/cmd/sdk/generators/go_test.go diff --git a/pkg/sdk/generators/php.go b/internal/cmd/sdk/generators/php.go similarity index 100% rename from pkg/sdk/generators/php.go rename to internal/cmd/sdk/generators/php.go diff --git a/pkg/sdk/generators/php_test.go b/internal/cmd/sdk/generators/php_test.go similarity index 100% rename from pkg/sdk/generators/php_test.go rename to internal/cmd/sdk/generators/php_test.go diff --git a/pkg/sdk/generators/python.go b/internal/cmd/sdk/generators/python.go similarity index 100% rename from pkg/sdk/generators/python.go rename to internal/cmd/sdk/generators/python.go diff --git a/pkg/sdk/generators/python_test.go b/internal/cmd/sdk/generators/python_test.go similarity index 100% rename from pkg/sdk/generators/python_test.go rename to internal/cmd/sdk/generators/python_test.go diff --git a/pkg/sdk/generators/typescript.go b/internal/cmd/sdk/generators/typescript.go similarity index 100% rename from pkg/sdk/generators/typescript.go rename to internal/cmd/sdk/generators/typescript.go diff --git a/pkg/sdk/generators/typescript_test.go b/internal/cmd/sdk/generators/typescript_test.go similarity index 100% rename from pkg/sdk/generators/typescript_test.go rename to internal/cmd/sdk/generators/typescript_test.go diff --git a/pkg/sdk/sdk.go b/internal/cmd/sdk/sdk.go similarity index 98% rename from pkg/sdk/sdk.go rename to internal/cmd/sdk/sdk.go index 1ed43fc..b5996de 100644 --- a/pkg/sdk/sdk.go +++ b/internal/cmd/sdk/sdk.go @@ -6,7 +6,7 @@ import ( "fmt" "path/filepath" - "github.com/host-uk/core/pkg/sdk/generators" + "github.com/host-uk/core/internal/cmd/sdk/generators" ) // Config holds SDK generation configuration from .core/release.yaml. diff --git a/pkg/sdk/sdk_test.go b/internal/cmd/sdk/sdk_test.go similarity index 100% rename from pkg/sdk/sdk_test.go rename to internal/cmd/sdk/sdk_test.go diff --git a/pkg/security/cmd.go b/internal/cmd/security/cmd.go similarity index 100% rename from pkg/security/cmd.go rename to internal/cmd/security/cmd.go diff --git a/pkg/security/cmd_alerts.go b/internal/cmd/security/cmd_alerts.go similarity index 100% rename from pkg/security/cmd_alerts.go rename to internal/cmd/security/cmd_alerts.go diff --git a/pkg/security/cmd_deps.go b/internal/cmd/security/cmd_deps.go similarity index 100% rename from pkg/security/cmd_deps.go rename to internal/cmd/security/cmd_deps.go diff --git a/pkg/security/cmd_scan.go b/internal/cmd/security/cmd_scan.go similarity index 100% rename from pkg/security/cmd_scan.go rename to internal/cmd/security/cmd_scan.go diff --git a/pkg/security/cmd_secrets.go b/internal/cmd/security/cmd_secrets.go similarity index 100% rename from pkg/security/cmd_secrets.go rename to internal/cmd/security/cmd_secrets.go diff --git a/pkg/security/cmd_security.go b/internal/cmd/security/cmd_security.go similarity index 100% rename from pkg/security/cmd_security.go rename to internal/cmd/security/cmd_security.go diff --git a/pkg/setup/cmd_bootstrap.go b/internal/cmd/setup/cmd_bootstrap.go similarity index 99% rename from pkg/setup/cmd_bootstrap.go rename to internal/cmd/setup/cmd_bootstrap.go index d6e6dfb..2e902b4 100644 --- a/pkg/setup/cmd_bootstrap.go +++ b/internal/cmd/setup/cmd_bootstrap.go @@ -13,9 +13,9 @@ import ( "path/filepath" "strings" + "github.com/host-uk/core/internal/cmd/workspace" "github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/repos" - "github.com/host-uk/core/pkg/workspace" ) // runSetupOrchestrator decides between registry mode and bootstrap mode. diff --git a/pkg/setup/cmd_ci.go b/internal/cmd/setup/cmd_ci.go similarity index 100% rename from pkg/setup/cmd_ci.go rename to internal/cmd/setup/cmd_ci.go diff --git a/pkg/setup/cmd_commands.go b/internal/cmd/setup/cmd_commands.go similarity index 100% rename from pkg/setup/cmd_commands.go rename to internal/cmd/setup/cmd_commands.go diff --git a/pkg/setup/cmd_github.go b/internal/cmd/setup/cmd_github.go similarity index 100% rename from pkg/setup/cmd_github.go rename to internal/cmd/setup/cmd_github.go diff --git a/pkg/setup/cmd_registry.go b/internal/cmd/setup/cmd_registry.go similarity index 99% rename from pkg/setup/cmd_registry.go rename to internal/cmd/setup/cmd_registry.go index 250cd0f..e68fc2b 100644 --- a/pkg/setup/cmd_registry.go +++ b/internal/cmd/setup/cmd_registry.go @@ -13,10 +13,10 @@ import ( "path/filepath" "strings" + "github.com/host-uk/core/internal/cmd/workspace" "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/repos" - "github.com/host-uk/core/pkg/workspace" ) // runRegistrySetup loads a registry from path and runs setup. diff --git a/pkg/setup/cmd_repo.go b/internal/cmd/setup/cmd_repo.go similarity index 100% rename from pkg/setup/cmd_repo.go rename to internal/cmd/setup/cmd_repo.go diff --git a/pkg/setup/cmd_setup.go b/internal/cmd/setup/cmd_setup.go similarity index 100% rename from pkg/setup/cmd_setup.go rename to internal/cmd/setup/cmd_setup.go diff --git a/pkg/setup/cmd_wizard.go b/internal/cmd/setup/cmd_wizard.go similarity index 100% rename from pkg/setup/cmd_wizard.go rename to internal/cmd/setup/cmd_wizard.go diff --git a/pkg/setup/github_config.go b/internal/cmd/setup/github_config.go similarity index 100% rename from pkg/setup/github_config.go rename to internal/cmd/setup/github_config.go diff --git a/pkg/setup/github_diff.go b/internal/cmd/setup/github_diff.go similarity index 100% rename from pkg/setup/github_diff.go rename to internal/cmd/setup/github_diff.go diff --git a/pkg/setup/github_labels.go b/internal/cmd/setup/github_labels.go similarity index 100% rename from pkg/setup/github_labels.go rename to internal/cmd/setup/github_labels.go diff --git a/pkg/setup/github_protection.go b/internal/cmd/setup/github_protection.go similarity index 100% rename from pkg/setup/github_protection.go rename to internal/cmd/setup/github_protection.go diff --git a/pkg/setup/github_security.go b/internal/cmd/setup/github_security.go similarity index 100% rename from pkg/setup/github_security.go rename to internal/cmd/setup/github_security.go diff --git a/pkg/setup/github_webhooks.go b/internal/cmd/setup/github_webhooks.go similarity index 100% rename from pkg/setup/github_webhooks.go rename to internal/cmd/setup/github_webhooks.go diff --git a/pkg/test/cmd_commands.go b/internal/cmd/test/cmd_commands.go similarity index 100% rename from pkg/test/cmd_commands.go rename to internal/cmd/test/cmd_commands.go diff --git a/pkg/test/cmd_main.go b/internal/cmd/test/cmd_main.go similarity index 100% rename from pkg/test/cmd_main.go rename to internal/cmd/test/cmd_main.go diff --git a/pkg/test/cmd_output.go b/internal/cmd/test/cmd_output.go similarity index 100% rename from pkg/test/cmd_output.go rename to internal/cmd/test/cmd_output.go diff --git a/pkg/test/cmd_runner.go b/internal/cmd/test/cmd_runner.go similarity index 100% rename from pkg/test/cmd_runner.go rename to internal/cmd/test/cmd_runner.go diff --git a/pkg/updater/.github/workflows/ci.yml b/internal/cmd/updater/.github/workflows/ci.yml similarity index 100% rename from pkg/updater/.github/workflows/ci.yml rename to internal/cmd/updater/.github/workflows/ci.yml diff --git a/pkg/updater/.github/workflows/release.yml b/internal/cmd/updater/.github/workflows/release.yml similarity index 100% rename from pkg/updater/.github/workflows/release.yml rename to internal/cmd/updater/.github/workflows/release.yml diff --git a/pkg/updater/.gitignore b/internal/cmd/updater/.gitignore similarity index 100% rename from pkg/updater/.gitignore rename to internal/cmd/updater/.gitignore diff --git a/pkg/updater/LICENSE b/internal/cmd/updater/LICENSE similarity index 100% rename from pkg/updater/LICENSE rename to internal/cmd/updater/LICENSE diff --git a/pkg/updater/Makefile b/internal/cmd/updater/Makefile similarity index 100% rename from pkg/updater/Makefile rename to internal/cmd/updater/Makefile diff --git a/pkg/updater/README.md b/internal/cmd/updater/README.md similarity index 100% rename from pkg/updater/README.md rename to internal/cmd/updater/README.md diff --git a/pkg/updater/build/main.go b/internal/cmd/updater/build/main.go similarity index 100% rename from pkg/updater/build/main.go rename to internal/cmd/updater/build/main.go diff --git a/pkg/updater/cmd.go b/internal/cmd/updater/cmd.go similarity index 100% rename from pkg/updater/cmd.go rename to internal/cmd/updater/cmd.go diff --git a/pkg/updater/cmd_unix.go b/internal/cmd/updater/cmd_unix.go similarity index 100% rename from pkg/updater/cmd_unix.go rename to internal/cmd/updater/cmd_unix.go diff --git a/pkg/updater/cmd_windows.go b/internal/cmd/updater/cmd_windows.go similarity index 100% rename from pkg/updater/cmd_windows.go rename to internal/cmd/updater/cmd_windows.go diff --git a/pkg/updater/docs/README.md b/internal/cmd/updater/docs/README.md similarity index 100% rename from pkg/updater/docs/README.md rename to internal/cmd/updater/docs/README.md diff --git a/pkg/updater/docs/architecture.md b/internal/cmd/updater/docs/architecture.md similarity index 100% rename from pkg/updater/docs/architecture.md rename to internal/cmd/updater/docs/architecture.md diff --git a/pkg/updater/docs/configuration.md b/internal/cmd/updater/docs/configuration.md similarity index 100% rename from pkg/updater/docs/configuration.md rename to internal/cmd/updater/docs/configuration.md diff --git a/pkg/updater/docs/getting-started.md b/internal/cmd/updater/docs/getting-started.md similarity index 100% rename from pkg/updater/docs/getting-started.md rename to internal/cmd/updater/docs/getting-started.md diff --git a/pkg/updater/generic_http.go b/internal/cmd/updater/generic_http.go similarity index 100% rename from pkg/updater/generic_http.go rename to internal/cmd/updater/generic_http.go diff --git a/pkg/updater/generic_http_test.go b/internal/cmd/updater/generic_http_test.go similarity index 100% rename from pkg/updater/generic_http_test.go rename to internal/cmd/updater/generic_http_test.go diff --git a/pkg/updater/github.go b/internal/cmd/updater/github.go similarity index 100% rename from pkg/updater/github.go rename to internal/cmd/updater/github.go diff --git a/pkg/updater/github_test.go b/internal/cmd/updater/github_test.go similarity index 100% rename from pkg/updater/github_test.go rename to internal/cmd/updater/github_test.go diff --git a/pkg/updater/mock_github_client_test.go b/internal/cmd/updater/mock_github_client_test.go similarity index 100% rename from pkg/updater/mock_github_client_test.go rename to internal/cmd/updater/mock_github_client_test.go diff --git a/pkg/updater/package.json b/internal/cmd/updater/package.json similarity index 100% rename from pkg/updater/package.json rename to internal/cmd/updater/package.json diff --git a/pkg/updater/service.go b/internal/cmd/updater/service.go similarity index 98% rename from pkg/updater/service.go rename to internal/cmd/updater/service.go index 4c57066..8251c03 100644 --- a/pkg/updater/service.go +++ b/internal/cmd/updater/service.go @@ -1,4 +1,4 @@ -//go:generate go run github.com/host-uk/core/pkg/updater/build +//go:generate go run github.com/host-uk/core/internal/cmd/updater/build // Package updater provides functionality for self-updating Go applications. // It supports updates from GitHub releases and generic HTTP endpoints. diff --git a/pkg/updater/service_examples_test.go b/internal/cmd/updater/service_examples_test.go similarity index 95% rename from pkg/updater/service_examples_test.go rename to internal/cmd/updater/service_examples_test.go index 542697a..6619eda 100644 --- a/pkg/updater/service_examples_test.go +++ b/internal/cmd/updater/service_examples_test.go @@ -4,7 +4,7 @@ import ( "fmt" "log" - "github.com/host-uk/core/pkg/updater" + "github.com/host-uk/core/internal/cmd/updater" ) func ExampleNewUpdateService() { diff --git a/pkg/updater/service_test.go b/internal/cmd/updater/service_test.go similarity index 100% rename from pkg/updater/service_test.go rename to internal/cmd/updater/service_test.go diff --git a/pkg/updater/tests.patch b/internal/cmd/updater/tests.patch similarity index 100% rename from pkg/updater/tests.patch rename to internal/cmd/updater/tests.patch diff --git a/pkg/updater/ui/.editorconfig b/internal/cmd/updater/ui/.editorconfig similarity index 100% rename from pkg/updater/ui/.editorconfig rename to internal/cmd/updater/ui/.editorconfig diff --git a/pkg/updater/ui/.gitignore b/internal/cmd/updater/ui/.gitignore similarity index 100% rename from pkg/updater/ui/.gitignore rename to internal/cmd/updater/ui/.gitignore diff --git a/pkg/updater/ui/.vscode/extensions.json b/internal/cmd/updater/ui/.vscode/extensions.json similarity index 100% rename from pkg/updater/ui/.vscode/extensions.json rename to internal/cmd/updater/ui/.vscode/extensions.json diff --git a/pkg/updater/ui/.vscode/launch.json b/internal/cmd/updater/ui/.vscode/launch.json similarity index 100% rename from pkg/updater/ui/.vscode/launch.json rename to internal/cmd/updater/ui/.vscode/launch.json diff --git a/pkg/updater/ui/.vscode/tasks.json b/internal/cmd/updater/ui/.vscode/tasks.json similarity index 100% rename from pkg/updater/ui/.vscode/tasks.json rename to internal/cmd/updater/ui/.vscode/tasks.json diff --git a/pkg/updater/ui/README.md b/internal/cmd/updater/ui/README.md similarity index 100% rename from pkg/updater/ui/README.md rename to internal/cmd/updater/ui/README.md diff --git a/pkg/updater/ui/angular.json b/internal/cmd/updater/ui/angular.json similarity index 100% rename from pkg/updater/ui/angular.json rename to internal/cmd/updater/ui/angular.json diff --git a/pkg/updater/ui/package-lock.json b/internal/cmd/updater/ui/package-lock.json similarity index 100% rename from pkg/updater/ui/package-lock.json rename to internal/cmd/updater/ui/package-lock.json diff --git a/pkg/updater/ui/package.json b/internal/cmd/updater/ui/package.json similarity index 100% rename from pkg/updater/ui/package.json rename to internal/cmd/updater/ui/package.json diff --git a/pkg/updater/ui/public/favicon.ico b/internal/cmd/updater/ui/public/favicon.ico similarity index 100% rename from pkg/updater/ui/public/favicon.ico rename to internal/cmd/updater/ui/public/favicon.ico diff --git a/pkg/updater/ui/src/app/app-module.ts b/internal/cmd/updater/ui/src/app/app-module.ts similarity index 100% rename from pkg/updater/ui/src/app/app-module.ts rename to internal/cmd/updater/ui/src/app/app-module.ts diff --git a/pkg/updater/ui/src/app/app.html b/internal/cmd/updater/ui/src/app/app.html similarity index 100% rename from pkg/updater/ui/src/app/app.html rename to internal/cmd/updater/ui/src/app/app.html diff --git a/pkg/updater/ui/src/app/app.ts b/internal/cmd/updater/ui/src/app/app.ts similarity index 100% rename from pkg/updater/ui/src/app/app.ts rename to internal/cmd/updater/ui/src/app/app.ts diff --git a/pkg/updater/ui/src/index.html b/internal/cmd/updater/ui/src/index.html similarity index 100% rename from pkg/updater/ui/src/index.html rename to internal/cmd/updater/ui/src/index.html diff --git a/pkg/updater/ui/src/main.ts b/internal/cmd/updater/ui/src/main.ts similarity index 100% rename from pkg/updater/ui/src/main.ts rename to internal/cmd/updater/ui/src/main.ts diff --git a/pkg/updater/ui/src/styles.css b/internal/cmd/updater/ui/src/styles.css similarity index 100% rename from pkg/updater/ui/src/styles.css rename to internal/cmd/updater/ui/src/styles.css diff --git a/pkg/updater/ui/tsconfig.app.json b/internal/cmd/updater/ui/tsconfig.app.json similarity index 100% rename from pkg/updater/ui/tsconfig.app.json rename to internal/cmd/updater/ui/tsconfig.app.json diff --git a/pkg/updater/ui/tsconfig.json b/internal/cmd/updater/ui/tsconfig.json similarity index 100% rename from pkg/updater/ui/tsconfig.json rename to internal/cmd/updater/ui/tsconfig.json diff --git a/pkg/updater/ui/tsconfig.spec.json b/internal/cmd/updater/ui/tsconfig.spec.json similarity index 100% rename from pkg/updater/ui/tsconfig.spec.json rename to internal/cmd/updater/ui/tsconfig.spec.json diff --git a/pkg/updater/updater.go b/internal/cmd/updater/updater.go similarity index 100% rename from pkg/updater/updater.go rename to internal/cmd/updater/updater.go diff --git a/pkg/updater/updater_test.go b/internal/cmd/updater/updater_test.go similarity index 100% rename from pkg/updater/updater_test.go rename to internal/cmd/updater/updater_test.go diff --git a/pkg/vm/cmd_commands.go b/internal/cmd/vm/cmd_commands.go similarity index 100% rename from pkg/vm/cmd_commands.go rename to internal/cmd/vm/cmd_commands.go diff --git a/pkg/vm/cmd_container.go b/internal/cmd/vm/cmd_container.go similarity index 100% rename from pkg/vm/cmd_container.go rename to internal/cmd/vm/cmd_container.go diff --git a/pkg/vm/cmd_templates.go b/internal/cmd/vm/cmd_templates.go similarity index 100% rename from pkg/vm/cmd_templates.go rename to internal/cmd/vm/cmd_templates.go diff --git a/pkg/vm/cmd_vm.go b/internal/cmd/vm/cmd_vm.go similarity index 100% rename from pkg/vm/cmd_vm.go rename to internal/cmd/vm/cmd_vm.go diff --git a/pkg/workspace/cmd.go b/internal/cmd/workspace/cmd.go similarity index 100% rename from pkg/workspace/cmd.go rename to internal/cmd/workspace/cmd.go diff --git a/pkg/workspace/cmd_workspace.go b/internal/cmd/workspace/cmd_workspace.go similarity index 100% rename from pkg/workspace/cmd_workspace.go rename to internal/cmd/workspace/cmd_workspace.go diff --git a/pkg/workspace/config.go b/internal/cmd/workspace/config.go similarity index 100% rename from pkg/workspace/config.go rename to internal/cmd/workspace/config.go diff --git a/internal/variants/ci.go b/internal/variants/ci.go index 313dd47..ec7d0f2 100644 --- a/internal/variants/ci.go +++ b/internal/variants/ci.go @@ -16,8 +16,8 @@ package variants import ( // Commands via self-registration + _ "github.com/host-uk/core/internal/cmd/ci" + _ "github.com/host-uk/core/internal/cmd/doctor" + _ "github.com/host-uk/core/internal/cmd/sdk" _ "github.com/host-uk/core/pkg/build/buildcmd" - _ "github.com/host-uk/core/pkg/ci" - _ "github.com/host-uk/core/pkg/doctor" - _ "github.com/host-uk/core/pkg/sdk" ) diff --git a/internal/variants/full.go b/internal/variants/full.go index 38d16d8..0232c70 100644 --- a/internal/variants/full.go +++ b/internal/variants/full.go @@ -24,22 +24,22 @@ package variants import ( // Commands via self-registration - _ "github.com/host-uk/core/pkg/ai" + _ "github.com/host-uk/core/internal/cmd/ai" + _ "github.com/host-uk/core/internal/cmd/ci" + _ "github.com/host-uk/core/internal/cmd/dev" + _ "github.com/host-uk/core/internal/cmd/docs" + _ "github.com/host-uk/core/internal/cmd/doctor" + _ "github.com/host-uk/core/internal/cmd/gitcmd" + _ "github.com/host-uk/core/internal/cmd/go" + _ "github.com/host-uk/core/internal/cmd/php" + _ "github.com/host-uk/core/internal/cmd/pkgcmd" + _ "github.com/host-uk/core/internal/cmd/qa" + _ "github.com/host-uk/core/internal/cmd/sdk" + _ "github.com/host-uk/core/internal/cmd/security" + _ "github.com/host-uk/core/internal/cmd/setup" + _ "github.com/host-uk/core/internal/cmd/test" + _ "github.com/host-uk/core/internal/cmd/updater" + _ "github.com/host-uk/core/internal/cmd/vm" + _ "github.com/host-uk/core/internal/cmd/workspace" _ "github.com/host-uk/core/pkg/build/buildcmd" - _ "github.com/host-uk/core/pkg/ci" - _ "github.com/host-uk/core/pkg/dev" - _ "github.com/host-uk/core/pkg/docs" - _ "github.com/host-uk/core/pkg/doctor" - _ "github.com/host-uk/core/pkg/gitcmd" - _ "github.com/host-uk/core/pkg/go" - _ "github.com/host-uk/core/pkg/php" - _ "github.com/host-uk/core/pkg/pkgcmd" - _ "github.com/host-uk/core/pkg/qa" - _ "github.com/host-uk/core/pkg/sdk" - _ "github.com/host-uk/core/pkg/security" - _ "github.com/host-uk/core/pkg/setup" - _ "github.com/host-uk/core/pkg/test" - _ "github.com/host-uk/core/pkg/updater" - _ "github.com/host-uk/core/pkg/vm" - _ "github.com/host-uk/core/pkg/workspace" ) diff --git a/internal/variants/minimal.go b/internal/variants/minimal.go index 69f4bff..9163757 100644 --- a/internal/variants/minimal.go +++ b/internal/variants/minimal.go @@ -13,5 +13,5 @@ package variants import ( // Commands via self-registration - _ "github.com/host-uk/core/pkg/doctor" + _ "github.com/host-uk/core/internal/cmd/doctor" ) diff --git a/internal/variants/php.go b/internal/variants/php.go index c7a574d..ff18d3e 100644 --- a/internal/variants/php.go +++ b/internal/variants/php.go @@ -14,6 +14,6 @@ package variants import ( // Commands via self-registration - _ "github.com/host-uk/core/pkg/doctor" - _ "github.com/host-uk/core/pkg/php" + _ "github.com/host-uk/core/internal/cmd/doctor" + _ "github.com/host-uk/core/internal/cmd/php" ) diff --git a/pkg/build/buildcmd/cmd_sdk.go b/pkg/build/buildcmd/cmd_sdk.go index 8102293..29222bb 100644 --- a/pkg/build/buildcmd/cmd_sdk.go +++ b/pkg/build/buildcmd/cmd_sdk.go @@ -11,8 +11,8 @@ import ( "os" "strings" + "github.com/host-uk/core/internal/cmd/sdk" "github.com/host-uk/core/pkg/i18n" - "github.com/host-uk/core/pkg/sdk" ) // runBuildSDK handles the `core build sdk` command. diff --git a/pkg/io/client_test.go b/pkg/io/client_test.go new file mode 100644 index 0000000..1579460 --- /dev/null +++ b/pkg/io/client_test.go @@ -0,0 +1,139 @@ +package io + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// --- MockMedium Tests --- + +func TestNewMockMedium_Good(t *testing.T) { + m := NewMockMedium() + assert.NotNil(t, m) + assert.NotNil(t, m.Files) + assert.NotNil(t, m.Dirs) + assert.Empty(t, m.Files) + assert.Empty(t, m.Dirs) +} + +func TestMockMedium_Read_Good(t *testing.T) { + m := NewMockMedium() + m.Files["test.txt"] = "hello world" + content, err := m.Read("test.txt") + assert.NoError(t, err) + assert.Equal(t, "hello world", content) +} + +func TestMockMedium_Read_Bad(t *testing.T) { + m := NewMockMedium() + _, err := m.Read("nonexistent.txt") + assert.Error(t, err) +} + +func TestMockMedium_Write_Good(t *testing.T) { + m := NewMockMedium() + err := m.Write("test.txt", "content") + assert.NoError(t, err) + assert.Equal(t, "content", m.Files["test.txt"]) + + // Overwrite existing file + err = m.Write("test.txt", "new content") + assert.NoError(t, err) + assert.Equal(t, "new content", m.Files["test.txt"]) +} + +func TestMockMedium_EnsureDir_Good(t *testing.T) { + m := NewMockMedium() + err := m.EnsureDir("/path/to/dir") + assert.NoError(t, err) + assert.True(t, m.Dirs["/path/to/dir"]) +} + +func TestMockMedium_IsFile_Good(t *testing.T) { + m := NewMockMedium() + m.Files["exists.txt"] = "content" + + assert.True(t, m.IsFile("exists.txt")) + assert.False(t, m.IsFile("nonexistent.txt")) +} + +func TestMockMedium_FileGet_Good(t *testing.T) { + m := NewMockMedium() + m.Files["test.txt"] = "content" + content, err := m.FileGet("test.txt") + assert.NoError(t, err) + assert.Equal(t, "content", content) +} + +func TestMockMedium_FileSet_Good(t *testing.T) { + m := NewMockMedium() + err := m.FileSet("test.txt", "content") + assert.NoError(t, err) + assert.Equal(t, "content", m.Files["test.txt"]) +} + +// --- Wrapper Function Tests --- + +func TestRead_Good(t *testing.T) { + m := NewMockMedium() + m.Files["test.txt"] = "hello" + content, err := Read(m, "test.txt") + assert.NoError(t, err) + assert.Equal(t, "hello", content) +} + +func TestWrite_Good(t *testing.T) { + m := NewMockMedium() + err := Write(m, "test.txt", "hello") + assert.NoError(t, err) + assert.Equal(t, "hello", m.Files["test.txt"]) +} + +func TestEnsureDir_Good(t *testing.T) { + m := NewMockMedium() + err := EnsureDir(m, "/my/dir") + assert.NoError(t, err) + assert.True(t, m.Dirs["/my/dir"]) +} + +func TestIsFile_Good(t *testing.T) { + m := NewMockMedium() + m.Files["exists.txt"] = "content" + + assert.True(t, IsFile(m, "exists.txt")) + assert.False(t, IsFile(m, "nonexistent.txt")) +} + +func TestCopy_Good(t *testing.T) { + source := NewMockMedium() + dest := NewMockMedium() + source.Files["test.txt"] = "hello" + err := Copy(source, "test.txt", dest, "test.txt") + assert.NoError(t, err) + assert.Equal(t, "hello", dest.Files["test.txt"]) + + // Copy to different path + source.Files["original.txt"] = "content" + err = Copy(source, "original.txt", dest, "copied.txt") + assert.NoError(t, err) + assert.Equal(t, "content", dest.Files["copied.txt"]) +} + +func TestCopy_Bad(t *testing.T) { + source := NewMockMedium() + dest := NewMockMedium() + err := Copy(source, "nonexistent.txt", dest, "dest.txt") + assert.Error(t, err) +} + +// --- Local Global Tests --- + +func TestLocalGlobal_Good(t *testing.T) { + // io.Local should be initialized by init() + assert.NotNil(t, Local, "io.Local should be initialized") + + // Should be able to use it as a Medium + var m Medium = Local + assert.NotNil(t, m) +} diff --git a/pkg/io/io.go b/pkg/io/io.go new file mode 100644 index 0000000..7c5299e --- /dev/null +++ b/pkg/io/io.go @@ -0,0 +1,138 @@ +package io + +import ( + "errors" + + coreerr "github.com/host-uk/core/pkg/framework/core" + "github.com/host-uk/core/pkg/io/local" +) + +// Medium defines the standard interface for a storage backend. +// This allows for different implementations (e.g., local disk, S3, SFTP) +// to be used interchangeably. +type Medium interface { + // Read retrieves the content of a file as a string. + Read(path string) (string, error) + + // Write saves the given content to a file, overwriting it if it exists. + Write(path, content string) error + + // EnsureDir makes sure a directory exists, creating it if necessary. + EnsureDir(path string) error + + // IsFile checks if a path exists and is a regular file. + IsFile(path string) bool + + // FileGet is a convenience function that reads a file from the medium. + FileGet(path string) (string, error) + + // FileSet is a convenience function that writes a file to the medium. + FileSet(path, content string) error +} + +// Local is a pre-initialized medium for the local filesystem. +// It uses "/" as root, providing unsandboxed access to the filesystem. +// For sandboxed access, use NewSandboxed with a specific root path. +var Local Medium + +func init() { + var err error + Local, err = local.New("/") + if err != nil { + panic("io: failed to initialize Local medium: " + err.Error()) + } +} + +// NewSandboxed creates a new Medium sandboxed to the given root directory. +// All file operations are restricted to paths within the root. +// The root directory will be created if it doesn't exist. +func NewSandboxed(root string) (Medium, error) { + return local.New(root) +} + +// --- Helper Functions --- + +// Read retrieves the content of a file from the given medium. +func Read(m Medium, path string) (string, error) { + return m.Read(path) +} + +// Write saves the given content to a file in the given medium. +func Write(m Medium, path, content string) error { + return m.Write(path, content) +} + +// EnsureDir makes sure a directory exists in the given medium. +func EnsureDir(m Medium, path string) error { + return m.EnsureDir(path) +} + +// IsFile checks if a path exists and is a regular file in the given medium. +func IsFile(m Medium, path string) bool { + return m.IsFile(path) +} + +// Copy copies a file from one medium to another. +func Copy(src Medium, srcPath string, dst Medium, dstPath string) error { + content, err := src.Read(srcPath) + if err != nil { + return coreerr.E("io.Copy", "read failed: "+srcPath, err) + } + if err := dst.Write(dstPath, content); err != nil { + return coreerr.E("io.Copy", "write failed: "+dstPath, err) + } + return nil +} + +// --- MockMedium --- + +// MockMedium is an in-memory implementation of Medium for testing. +type MockMedium struct { + Files map[string]string + Dirs map[string]bool +} + +// NewMockMedium creates a new MockMedium instance. +func NewMockMedium() *MockMedium { + return &MockMedium{ + Files: make(map[string]string), + Dirs: make(map[string]bool), + } +} + +// Read retrieves the content of a file from the mock filesystem. +func (m *MockMedium) Read(path string) (string, error) { + content, ok := m.Files[path] + if !ok { + return "", coreerr.E("io.MockMedium.Read", "file not found: "+path, errors.New("file not found")) + } + return content, nil +} + +// Write saves the given content to a file in the mock filesystem. +func (m *MockMedium) Write(path, content string) error { + m.Files[path] = content + return nil +} + +// EnsureDir records that a directory exists in the mock filesystem. +func (m *MockMedium) EnsureDir(path string) error { + m.Dirs[path] = true + return nil +} + +// IsFile checks if a path exists as a file in the mock filesystem. +func (m *MockMedium) IsFile(path string) bool { + _, ok := m.Files[path] + return ok +} + +// FileGet is a convenience function that reads a file from the mock filesystem. +func (m *MockMedium) FileGet(path string) (string, error) { + return m.Read(path) +} + +// FileSet is a convenience function that writes a file to the mock filesystem. +func (m *MockMedium) FileSet(path, content string) error { + return m.Write(path, content) +} diff --git a/pkg/io/local/client.go b/pkg/io/local/client.go new file mode 100644 index 0000000..afe632e --- /dev/null +++ b/pkg/io/local/client.go @@ -0,0 +1,169 @@ +// Package local provides a local filesystem implementation of the io.Medium interface. +package local + +import ( + "errors" + "os" + "path/filepath" + "strings" +) + +// Medium is a local filesystem storage backend. +type Medium struct { + root string +} + +// New creates a new local Medium with the specified root directory. +// The root directory will be created if it doesn't exist. +func New(root string) (*Medium, error) { + // Ensure root is an absolute path + absRoot, err := filepath.Abs(root) + if err != nil { + return nil, err + } + + // Create root directory if it doesn't exist + if err := os.MkdirAll(absRoot, 0755); err != nil { + return nil, err + } + + return &Medium{root: absRoot}, nil +} + +// path sanitizes and joins the relative path with the root directory. +// Returns an error if a path traversal attempt is detected. +// Uses filepath.EvalSymlinks to prevent symlink-based bypass attacks. +func (m *Medium) path(relativePath string) (string, error) { + // Clean the path to remove any .. or . components + cleanPath := filepath.Clean(relativePath) + + // Check for path traversal attempts in the raw path + if strings.HasPrefix(cleanPath, "..") || strings.Contains(cleanPath, string(filepath.Separator)+"..") { + return "", errors.New("path traversal attempt detected") + } + + // Reject absolute paths - they bypass the sandbox + if filepath.IsAbs(cleanPath) { + return "", errors.New("path traversal attempt detected") + } + + fullPath := filepath.Join(m.root, cleanPath) + + // Verify the resulting path is still within root (boundary-aware check) + // Must use separator to prevent /tmp/root matching /tmp/root2 + rootWithSep := m.root + if !strings.HasSuffix(rootWithSep, string(filepath.Separator)) { + rootWithSep += string(filepath.Separator) + } + if fullPath != m.root && !strings.HasPrefix(fullPath, rootWithSep) { + return "", errors.New("path traversal attempt detected") + } + + // Resolve symlinks to prevent bypass attacks + // We need to resolve both the root and full path to handle symlinked roots + resolvedRoot, err := filepath.EvalSymlinks(m.root) + if err != nil { + return "", err + } + + // Build boundary-aware prefix for resolved root + resolvedRootWithSep := resolvedRoot + if !strings.HasSuffix(resolvedRootWithSep, string(filepath.Separator)) { + resolvedRootWithSep += string(filepath.Separator) + } + + // For the full path, resolve as much as exists + // Use Lstat first to check if the path exists + if _, err := os.Lstat(fullPath); err == nil { + resolvedPath, err := filepath.EvalSymlinks(fullPath) + if err != nil { + return "", err + } + // Verify resolved path is still within resolved root (boundary-aware) + if resolvedPath != resolvedRoot && !strings.HasPrefix(resolvedPath, resolvedRootWithSep) { + return "", errors.New("path traversal attempt detected via symlink") + } + return resolvedPath, nil + } + + // Path doesn't exist yet - verify parent directory + parentDir := filepath.Dir(fullPath) + if _, err := os.Lstat(parentDir); err == nil { + resolvedParent, err := filepath.EvalSymlinks(parentDir) + if err != nil { + return "", err + } + if resolvedParent != resolvedRoot && !strings.HasPrefix(resolvedParent, resolvedRootWithSep) { + return "", errors.New("path traversal attempt detected via symlink") + } + } + + return fullPath, nil +} + +// Read retrieves the content of a file as a string. +func (m *Medium) Read(relativePath string) (string, error) { + fullPath, err := m.path(relativePath) + if err != nil { + return "", err + } + + content, err := os.ReadFile(fullPath) + if err != nil { + return "", err + } + + return string(content), nil +} + +// Write saves the given content to a file, overwriting it if it exists. +// Parent directories are created automatically. +func (m *Medium) Write(relativePath, content string) error { + fullPath, err := m.path(relativePath) + if err != nil { + return err + } + + // Ensure parent directory exists + parentDir := filepath.Dir(fullPath) + if err := os.MkdirAll(parentDir, 0755); err != nil { + return err + } + + return os.WriteFile(fullPath, []byte(content), 0644) +} + +// EnsureDir makes sure a directory exists, creating it if necessary. +func (m *Medium) EnsureDir(relativePath string) error { + fullPath, err := m.path(relativePath) + if err != nil { + return err + } + + return os.MkdirAll(fullPath, 0755) +} + +// IsFile checks if a path exists and is a regular file. +func (m *Medium) IsFile(relativePath string) bool { + fullPath, err := m.path(relativePath) + if err != nil { + return false + } + + info, err := os.Stat(fullPath) + if err != nil { + return false + } + + return info.Mode().IsRegular() +} + +// FileGet is a convenience function that reads a file from the medium. +func (m *Medium) FileGet(relativePath string) (string, error) { + return m.Read(relativePath) +} + +// FileSet is a convenience function that writes a file to the medium. +func (m *Medium) FileSet(relativePath, content string) error { + return m.Write(relativePath, content) +} diff --git a/pkg/io/local/client_test.go b/pkg/io/local/client_test.go new file mode 100644 index 0000000..191f4f1 --- /dev/null +++ b/pkg/io/local/client_test.go @@ -0,0 +1,201 @@ +package local + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNew_Good(t *testing.T) { + testRoot := t.TempDir() + + // Test successful creation + medium, err := New(testRoot) + assert.NoError(t, err) + assert.NotNil(t, medium) + assert.Equal(t, testRoot, medium.root) + + // Verify the root directory exists + info, err := os.Stat(testRoot) + assert.NoError(t, err) + assert.True(t, info.IsDir()) + + // Test creating a new instance with an existing directory (should not error) + medium2, err := New(testRoot) + assert.NoError(t, err) + assert.NotNil(t, medium2) +} + +func TestPath_Good(t *testing.T) { + testRoot := t.TempDir() + medium := &Medium{root: testRoot} + + // Valid path + validPath, err := medium.path("file.txt") + assert.NoError(t, err) + assert.Equal(t, filepath.Join(testRoot, "file.txt"), validPath) + + // Subdirectory path + subDirPath, err := medium.path("dir/sub/file.txt") + assert.NoError(t, err) + assert.Equal(t, filepath.Join(testRoot, "dir", "sub", "file.txt"), subDirPath) +} + +func TestPath_Bad(t *testing.T) { + testRoot := t.TempDir() + medium := &Medium{root: testRoot} + + // Path traversal attempt + _, err := medium.path("../secret.txt") + assert.Error(t, err) + assert.Contains(t, err.Error(), "path traversal attempt detected") + + _, err = medium.path("dir/../../secret.txt") + assert.Error(t, err) + assert.Contains(t, err.Error(), "path traversal attempt detected") + + // Absolute path attempt + _, err = medium.path("/etc/passwd") + assert.Error(t, err) + assert.Contains(t, err.Error(), "path traversal attempt detected") +} + +func TestReadWrite_Good(t *testing.T) { + testRoot, err := os.MkdirTemp("", "local_read_write_test") + assert.NoError(t, err) + defer os.RemoveAll(testRoot) + + medium, err := New(testRoot) + assert.NoError(t, err) + + fileName := "testfile.txt" + filePath := filepath.Join("subdir", fileName) + content := "Hello, Gopher!\nThis is a test file." + + // Test Write + err = medium.Write(filePath, content) + assert.NoError(t, err) + + // Verify file content by reading directly from OS + readContent, err := os.ReadFile(filepath.Join(testRoot, filePath)) + assert.NoError(t, err) + assert.Equal(t, content, string(readContent)) + + // Test Read + readByMedium, err := medium.Read(filePath) + assert.NoError(t, err) + assert.Equal(t, content, readByMedium) + + // Test Read non-existent file + _, err = medium.Read("nonexistent.txt") + assert.Error(t, err) + assert.True(t, os.IsNotExist(err)) + + // Test Write to a path with traversal attempt + writeErr := medium.Write("../badfile.txt", "malicious content") + assert.Error(t, writeErr) + assert.Contains(t, writeErr.Error(), "path traversal attempt detected") +} + +func TestEnsureDir_Good(t *testing.T) { + testRoot, err := os.MkdirTemp("", "local_ensure_dir_test") + assert.NoError(t, err) + defer os.RemoveAll(testRoot) + + medium, err := New(testRoot) + assert.NoError(t, err) + + dirName := "newdir/subdir" + dirPath := filepath.Join(testRoot, dirName) + + // Test creating a new directory + err = medium.EnsureDir(dirName) + assert.NoError(t, err) + info, err := os.Stat(dirPath) + assert.NoError(t, err) + assert.True(t, info.IsDir()) + + // Test ensuring an existing directory (should not error) + err = medium.EnsureDir(dirName) + assert.NoError(t, err) + + // Test ensuring a directory with path traversal attempt + err = medium.EnsureDir("../bad_dir") + assert.Error(t, err) + assert.Contains(t, err.Error(), "path traversal attempt detected") +} + +func TestIsFile_Good(t *testing.T) { + testRoot, err := os.MkdirTemp("", "local_is_file_test") + assert.NoError(t, err) + defer os.RemoveAll(testRoot) + + medium, err := New(testRoot) + assert.NoError(t, err) + + // Create a test file + fileName := "existing_file.txt" + filePath := filepath.Join(testRoot, fileName) + err = os.WriteFile(filePath, []byte("content"), 0644) + assert.NoError(t, err) + + // Create a test directory + dirName := "existing_dir" + dirPath := filepath.Join(testRoot, dirName) + err = os.Mkdir(dirPath, 0755) + assert.NoError(t, err) + + // Test with an existing file + assert.True(t, medium.IsFile(fileName)) + + // Test with a non-existent file + assert.False(t, medium.IsFile("nonexistent_file.txt")) + + // Test with a directory + assert.False(t, medium.IsFile(dirName)) + + // Test with path traversal attempt + assert.False(t, medium.IsFile("../bad_file.txt")) +} + +func TestFileGetFileSet_Good(t *testing.T) { + testRoot, err := os.MkdirTemp("", "local_fileget_fileset_test") + assert.NoError(t, err) + defer os.RemoveAll(testRoot) + + medium, err := New(testRoot) + assert.NoError(t, err) + + fileName := "data.txt" + content := "Hello, FileGet/FileSet!" + + // Test FileSet + err = medium.FileSet(fileName, content) + assert.NoError(t, err) + + // Verify file was written + readContent, err := os.ReadFile(filepath.Join(testRoot, fileName)) + assert.NoError(t, err) + assert.Equal(t, content, string(readContent)) + + // Test FileGet + gotContent, err := medium.FileGet(fileName) + assert.NoError(t, err) + assert.Equal(t, content, gotContent) + + // Test FileGet on non-existent file + _, err = medium.FileGet("nonexistent.txt") + assert.Error(t, err) + + // Test FileSet with path traversal attempt + err = medium.FileSet("../bad.txt", "malicious") + assert.Error(t, err) + assert.Contains(t, err.Error(), "path traversal attempt detected") + + // Test FileGet with path traversal attempt + _, err = medium.FileGet("../bad.txt") + assert.Error(t, err) + assert.Contains(t, err.Error(), "path traversal attempt detected") +} diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index 7ef032f..2e4d7b5 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -9,17 +9,52 @@ import ( "path/filepath" "strings" + "github.com/host-uk/core/pkg/io" "github.com/modelcontextprotocol/go-sdk/mcp" ) // Service provides a lightweight MCP server with file operations only. // For full GUI features, use the core-gui package. type Service struct { - server *mcp.Server + server *mcp.Server + workspaceRoot string // Root directory for file operations (empty = unrestricted) + medium io.Medium // Filesystem medium for sandboxed operations +} + +// Option configures a Service. +type Option func(*Service) error + +// WithWorkspaceRoot restricts file operations to the given directory. +// All paths are validated to be within this directory. +// An empty string disables the restriction (not recommended). +func WithWorkspaceRoot(root string) Option { + return func(s *Service) error { + if root == "" { + // Explicitly disable restriction - use unsandboxed global + s.workspaceRoot = "" + s.medium = io.Local + return nil + } + // Create sandboxed medium for this workspace + abs, err := filepath.Abs(root) + if err != nil { + return fmt.Errorf("invalid workspace root: %w", err) + } + m, err := io.NewSandboxed(abs) + if err != nil { + return fmt.Errorf("failed to create workspace medium: %w", err) + } + s.workspaceRoot = abs + s.medium = m + return nil + } } // New creates a new MCP service with file operations. -func New() *Service { +// By default, restricts file access to the current working directory. +// Use WithWorkspaceRoot("") to disable restrictions (not recommended). +// Returns an error if initialization fails. +func New(opts ...Option) (*Service, error) { impl := &mcp.Implementation{ Name: "core-cli", Version: "0.1.0", @@ -27,8 +62,28 @@ func New() *Service { server := mcp.NewServer(impl, nil) s := &Service{server: server} + + // Default to current working directory with sandboxed medium + cwd, err := os.Getwd() + if err != nil { + return nil, fmt.Errorf("failed to get working directory: %w", err) + } + s.workspaceRoot = cwd + m, err := io.NewSandboxed(cwd) + if err != nil { + return nil, fmt.Errorf("failed to create sandboxed medium: %w", err) + } + s.medium = m + + // Apply options + for _, opt := range opts { + if err := opt(s); err != nil { + return nil, fmt.Errorf("failed to apply option: %w", err) + } + } + s.registerTools() - return s + return s, nil } // registerTools adds file operation tools to the MCP server. @@ -223,31 +278,33 @@ type EditDiffOutput struct { // Tool handlers func (s *Service) readFile(ctx context.Context, req *mcp.CallToolRequest, input ReadFileInput) (*mcp.CallToolResult, ReadFileOutput, error) { - content, err := os.ReadFile(input.Path) + content, err := s.medium.Read(input.Path) if err != nil { return nil, ReadFileOutput{}, fmt.Errorf("failed to read file: %w", err) } return nil, ReadFileOutput{ - Content: string(content), + Content: content, Language: detectLanguageFromPath(input.Path), Path: input.Path, }, nil } func (s *Service) writeFile(ctx context.Context, req *mcp.CallToolRequest, input WriteFileInput) (*mcp.CallToolResult, WriteFileOutput, error) { - dir := filepath.Dir(input.Path) - if err := os.MkdirAll(dir, 0755); err != nil { - return nil, WriteFileOutput{}, fmt.Errorf("failed to create directory: %w", err) - } - err := os.WriteFile(input.Path, []byte(input.Content), 0644) - if err != nil { + // Medium.Write creates parent directories automatically + if err := s.medium.Write(input.Path, input.Content); err != nil { return nil, WriteFileOutput{}, fmt.Errorf("failed to write file: %w", err) } return nil, WriteFileOutput{Success: true, Path: input.Path}, nil } func (s *Service) listDirectory(ctx context.Context, req *mcp.CallToolRequest, input ListDirectoryInput) (*mcp.CallToolResult, ListDirectoryOutput, error) { - entries, err := os.ReadDir(input.Path) + // For directory listing, we need to use the underlying filesystem + // The Medium interface doesn't have a list method, so we validate and use os.ReadDir + path, err := s.resolvePath(input.Path) + if err != nil { + return nil, ListDirectoryOutput{}, err + } + entries, err := os.ReadDir(path) if err != nil { return nil, ListDirectoryOutput{}, fmt.Errorf("failed to list directory: %w", err) } @@ -269,31 +326,51 @@ func (s *Service) listDirectory(ctx context.Context, req *mcp.CallToolRequest, i } func (s *Service) createDirectory(ctx context.Context, req *mcp.CallToolRequest, input CreateDirectoryInput) (*mcp.CallToolResult, CreateDirectoryOutput, error) { - err := os.MkdirAll(input.Path, 0755) - if err != nil { + if err := s.medium.EnsureDir(input.Path); err != nil { return nil, CreateDirectoryOutput{}, fmt.Errorf("failed to create directory: %w", err) } return nil, CreateDirectoryOutput{Success: true, Path: input.Path}, nil } func (s *Service) deleteFile(ctx context.Context, req *mcp.CallToolRequest, input DeleteFileInput) (*mcp.CallToolResult, DeleteFileOutput, error) { - err := os.Remove(input.Path) + // Medium interface doesn't have delete, use resolved path with os.Remove + path, err := s.resolvePath(input.Path) if err != nil { + return nil, DeleteFileOutput{}, err + } + if err := os.Remove(path); err != nil { return nil, DeleteFileOutput{}, fmt.Errorf("failed to delete file: %w", err) } return nil, DeleteFileOutput{Success: true, Path: input.Path}, nil } func (s *Service) renameFile(ctx context.Context, req *mcp.CallToolRequest, input RenameFileInput) (*mcp.CallToolResult, RenameFileOutput, error) { - err := os.Rename(input.OldPath, input.NewPath) + // Medium interface doesn't have rename, use resolved paths with os.Rename + oldPath, err := s.resolvePath(input.OldPath) if err != nil { + return nil, RenameFileOutput{}, err + } + newPath, err := s.resolvePath(input.NewPath) + if err != nil { + return nil, RenameFileOutput{}, err + } + if err := os.Rename(oldPath, newPath); err != nil { return nil, RenameFileOutput{}, fmt.Errorf("failed to rename file: %w", err) } return nil, RenameFileOutput{Success: true, OldPath: input.OldPath, NewPath: input.NewPath}, nil } func (s *Service) fileExists(ctx context.Context, req *mcp.CallToolRequest, input FileExistsInput) (*mcp.CallToolResult, FileExistsOutput, error) { - info, err := os.Stat(input.Path) + exists := s.medium.IsFile(input.Path) + if exists { + return nil, FileExistsOutput{Exists: true, IsDir: false, Path: input.Path}, nil + } + // Check if it's a directory + path, err := s.resolvePath(input.Path) + if err != nil { + return nil, FileExistsOutput{}, err + } + info, err := os.Stat(path) if os.IsNotExist(err) { return nil, FileExistsOutput{Exists: false, IsDir: false, Path: input.Path}, nil } @@ -334,30 +411,28 @@ func (s *Service) editDiff(ctx context.Context, req *mcp.CallToolRequest, input return nil, EditDiffOutput{}, fmt.Errorf("old_string cannot be empty") } - content, err := os.ReadFile(input.Path) + content, err := s.medium.Read(input.Path) if err != nil { return nil, EditDiffOutput{}, fmt.Errorf("failed to read file: %w", err) } - fileContent := string(content) count := 0 if input.ReplaceAll { - count = strings.Count(fileContent, input.OldString) + count = strings.Count(content, input.OldString) if count == 0 { return nil, EditDiffOutput{}, fmt.Errorf("old_string not found in file") } - fileContent = strings.ReplaceAll(fileContent, input.OldString, input.NewString) + content = strings.ReplaceAll(content, input.OldString, input.NewString) } else { - if !strings.Contains(fileContent, input.OldString) { + if !strings.Contains(content, input.OldString) { return nil, EditDiffOutput{}, fmt.Errorf("old_string not found in file") } - fileContent = strings.Replace(fileContent, input.OldString, input.NewString, 1) + content = strings.Replace(content, input.OldString, input.NewString, 1) count = 1 } - err = os.WriteFile(input.Path, []byte(fileContent), 0644) - if err != nil { + if err := s.medium.Write(input.Path, content); err != nil { return nil, EditDiffOutput{}, fmt.Errorf("failed to write file: %w", err) } @@ -368,6 +443,73 @@ func (s *Service) editDiff(ctx context.Context, req *mcp.CallToolRequest, input }, nil } +// resolvePath converts a relative path to absolute using the workspace root. +// For operations not covered by Medium interface, this provides the full path. +// Returns an error if the path is outside the workspace root. +func (s *Service) resolvePath(path string) (string, error) { + if s.workspaceRoot == "" { + // Unrestricted mode + if filepath.IsAbs(path) { + return filepath.Clean(path), nil + } + abs, err := filepath.Abs(path) + if err != nil { + return "", fmt.Errorf("invalid path: %w", err) + } + return abs, nil + } + + var absPath string + if filepath.IsAbs(path) { + absPath = filepath.Clean(path) + } else { + absPath = filepath.Join(s.workspaceRoot, path) + } + + // Resolve symlinks for security + resolvedRoot, err := filepath.EvalSymlinks(s.workspaceRoot) + if err != nil { + return "", fmt.Errorf("failed to resolve workspace root: %w", err) + } + + // Build boundary-aware prefix + rootWithSep := resolvedRoot + if !strings.HasSuffix(rootWithSep, string(filepath.Separator)) { + rootWithSep += string(filepath.Separator) + } + + // Check if path exists to resolve symlinks + if _, err := os.Lstat(absPath); err == nil { + resolvedPath, err := filepath.EvalSymlinks(absPath) + if err != nil { + return "", fmt.Errorf("failed to resolve path: %w", err) + } + if resolvedPath != resolvedRoot && !strings.HasPrefix(resolvedPath, rootWithSep) { + return "", fmt.Errorf("path outside workspace: %s", path) + } + return resolvedPath, nil + } + + // Path doesn't exist - verify parent directory + parentDir := filepath.Dir(absPath) + if _, err := os.Lstat(parentDir); err == nil { + resolvedParent, err := filepath.EvalSymlinks(parentDir) + if err != nil { + return "", fmt.Errorf("failed to resolve parent: %w", err) + } + if resolvedParent != resolvedRoot && !strings.HasPrefix(resolvedParent, rootWithSep) { + return "", fmt.Errorf("path outside workspace: %s", path) + } + } + + // Verify the cleaned path is within workspace + if absPath != s.workspaceRoot && !strings.HasPrefix(absPath, rootWithSep) { + return "", fmt.Errorf("path outside workspace: %s", path) + } + + return absPath, nil +} + // detectLanguageFromPath maps file extensions to language IDs. func detectLanguageFromPath(path string) string { ext := filepath.Ext(path) diff --git a/pkg/mcp/mcp_test.go b/pkg/mcp/mcp_test.go new file mode 100644 index 0000000..4d33d7c --- /dev/null +++ b/pkg/mcp/mcp_test.go @@ -0,0 +1,217 @@ +package mcp + +import ( + "os" + "path/filepath" + "testing" +) + +func TestNew_Good_DefaultWorkspace(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get working directory: %v", err) + } + + s, err := New() + if err != nil { + t.Fatalf("Failed to create service: %v", err) + } + + if s.workspaceRoot != cwd { + t.Errorf("Expected default workspace root %s, got %s", cwd, s.workspaceRoot) + } + if s.medium == nil { + t.Error("Expected medium to be set") + } +} + +func TestNew_Good_CustomWorkspace(t *testing.T) { + tmpDir := t.TempDir() + + s, err := New(WithWorkspaceRoot(tmpDir)) + if err != nil { + t.Fatalf("Failed to create service: %v", err) + } + + if s.workspaceRoot != tmpDir { + t.Errorf("Expected workspace root %s, got %s", tmpDir, s.workspaceRoot) + } + if s.medium == nil { + t.Error("Expected medium to be set") + } +} + +func TestNew_Good_NoRestriction(t *testing.T) { + s, err := New(WithWorkspaceRoot("")) + if err != nil { + t.Fatalf("Failed to create service: %v", err) + } + + if s.workspaceRoot != "" { + t.Errorf("Expected empty workspace root, got %s", s.workspaceRoot) + } + if s.medium == nil { + t.Error("Expected medium to be set (unsandboxed)") + } +} + +func TestMedium_Good_ReadWrite(t *testing.T) { + tmpDir := t.TempDir() + s, err := New(WithWorkspaceRoot(tmpDir)) + if err != nil { + t.Fatalf("Failed to create service: %v", err) + } + + // Write a file + testContent := "hello world" + err = s.medium.Write("test.txt", testContent) + if err != nil { + t.Fatalf("Failed to write file: %v", err) + } + + // Read it back + content, err := s.medium.Read("test.txt") + if err != nil { + t.Fatalf("Failed to read file: %v", err) + } + if content != testContent { + t.Errorf("Expected content %q, got %q", testContent, content) + } + + // Verify file exists on disk + diskPath := filepath.Join(tmpDir, "test.txt") + if _, err := os.Stat(diskPath); os.IsNotExist(err) { + t.Error("File should exist on disk") + } +} + +func TestMedium_Good_EnsureDir(t *testing.T) { + tmpDir := t.TempDir() + s, err := New(WithWorkspaceRoot(tmpDir)) + if err != nil { + t.Fatalf("Failed to create service: %v", err) + } + + err = s.medium.EnsureDir("subdir/nested") + if err != nil { + t.Fatalf("Failed to create directory: %v", err) + } + + // Verify directory exists + diskPath := filepath.Join(tmpDir, "subdir", "nested") + info, err := os.Stat(diskPath) + if os.IsNotExist(err) { + t.Error("Directory should exist on disk") + } + if err == nil && !info.IsDir() { + t.Error("Path should be a directory") + } +} + +func TestMedium_Good_IsFile(t *testing.T) { + tmpDir := t.TempDir() + s, err := New(WithWorkspaceRoot(tmpDir)) + if err != nil { + t.Fatalf("Failed to create service: %v", err) + } + + // File doesn't exist yet + if s.medium.IsFile("test.txt") { + t.Error("File should not exist yet") + } + + // Create the file + _ = s.medium.Write("test.txt", "content") + + // Now it should exist + if !s.medium.IsFile("test.txt") { + t.Error("File should exist after write") + } +} + +func TestResolvePath_Good(t *testing.T) { + tmpDir := t.TempDir() + s, err := New(WithWorkspaceRoot(tmpDir)) + if err != nil { + t.Fatalf("Failed to create service: %v", err) + } + + // Write a test file so resolve can work + _ = s.medium.Write("test.txt", "content") + + // Relative path should resolve to workspace + resolved, err := s.resolvePath("test.txt") + if err != nil { + t.Fatalf("Failed to resolve path: %v", err) + } + // The resolved path may be the symlink-resolved version + if !filepath.IsAbs(resolved) { + t.Errorf("Expected absolute path, got %s", resolved) + } +} + +func TestResolvePath_Good_NoWorkspace(t *testing.T) { + s, err := New(WithWorkspaceRoot("")) + if err != nil { + t.Fatalf("Failed to create service: %v", err) + } + + // With no workspace, relative paths resolve to cwd + cwd, _ := os.Getwd() + resolved, err := s.resolvePath("test.txt") + if err != nil { + t.Fatalf("Failed to resolve path: %v", err) + } + expected := filepath.Join(cwd, "test.txt") + if resolved != expected { + t.Errorf("Expected %s, got %s", expected, resolved) + } +} + +func TestResolvePath_Bad_Traversal(t *testing.T) { + tmpDir := t.TempDir() + s, err := New(WithWorkspaceRoot(tmpDir)) + if err != nil { + t.Fatalf("Failed to create service: %v", err) + } + + // Path traversal should fail + _, err = s.resolvePath("../secret.txt") + if err == nil { + t.Error("Expected error for path traversal") + } + + // Absolute path outside workspace should fail + _, err = s.resolvePath("/etc/passwd") + if err == nil { + t.Error("Expected error for absolute path outside workspace") + } +} + +func TestResolvePath_Bad_SymlinkTraversal(t *testing.T) { + tmpDir := t.TempDir() + outsideDir := t.TempDir() + + // Create a target file outside workspace + targetFile := filepath.Join(outsideDir, "secret.txt") + if err := os.WriteFile(targetFile, []byte("secret"), 0644); err != nil { + t.Fatalf("Failed to create target file: %v", err) + } + + // Create symlink inside workspace pointing outside + symlinkPath := filepath.Join(tmpDir, "evil-link") + if err := os.Symlink(targetFile, symlinkPath); err != nil { + t.Skipf("Symlinks not supported: %v", err) + } + + s, err := New(WithWorkspaceRoot(tmpDir)) + if err != nil { + t.Fatalf("Failed to create service: %v", err) + } + + // Symlink traversal should be blocked + _, err = s.resolvePath("evil-link") + if err == nil { + t.Error("Expected error for symlink pointing outside workspace") + } +} diff --git a/pkg/release/sdk.go b/pkg/release/sdk.go index 420e02f..6f965ff 100644 --- a/pkg/release/sdk.go +++ b/pkg/release/sdk.go @@ -5,7 +5,7 @@ import ( "context" "fmt" - "github.com/host-uk/core/pkg/sdk" + "github.com/host-uk/core/internal/cmd/sdk" ) // SDKRelease holds the result of an SDK release.