├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── bug.yml │ ├── proposal.yml │ └── failed-installation.yml ├── demo.tape └── workflows │ ├── demo-animation.yml │ └── test.yml ├── demo.gif ├── internal ├── pkg │ ├── global │ │ └── version.go │ ├── handlers │ │ ├── legal.go │ │ ├── redirect.go │ │ ├── badge.go │ │ ├── util.go │ │ ├── api.go │ │ ├── sitemap.go │ │ ├── page.go │ │ └── installation.go │ ├── platforms │ │ ├── platforms.go │ │ └── parser.go │ └── config │ │ └── config.go └── metrics │ └── metrics.go ├── static ├── favicon.ico ├── robots.txt ├── favicon-16x16.png ├── favicon-32x32.png ├── mstile-150x150.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-384x384.png ├── browserconfig.xml ├── css │ └── global.css ├── site.webmanifest └── safari-pinned-tab.svg ├── scripts ├── embed.go ├── assets │ ├── linux │ │ ├── lib │ │ │ ├── map.sh │ │ │ └── colors.sh │ │ └── script.sh │ ├── shared │ │ └── intro.ps1 │ ├── footer.txt │ ├── header.txt │ └── windows │ │ ├── lib │ │ └── colors.ps1 │ │ └── script.ps1 ├── templates.go └── combine.go ├── templates ├── lib │ ├── base.gohtml │ ├── footer.gohtml │ ├── nav.gohtml │ └── head.gohtml ├── templates.go ├── stats.gohtml ├── privacy.gohtml ├── tos.gohtml ├── repo.gohtml ├── home.gohtml └── repo-maintainer.gohtml ├── .gitignore ├── Dockerfile ├── go.mod ├── Makefile ├── LICENSE ├── README.md ├── go.sum ├── main.go └── CODE_OF_CONDUCT.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: MarvinJWendt 2 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/installer/instl/HEAD/demo.gif -------------------------------------------------------------------------------- /internal/pkg/global/version.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | var Version = "dev" 4 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/installer/instl/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | 4 | Sitemap: https://instl.sh/sitemap.xml 5 | -------------------------------------------------------------------------------- /static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/installer/instl/HEAD/static/favicon-16x16.png -------------------------------------------------------------------------------- /static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/installer/instl/HEAD/static/favicon-32x32.png -------------------------------------------------------------------------------- /static/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/installer/instl/HEAD/static/mstile-150x150.png -------------------------------------------------------------------------------- /static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/installer/instl/HEAD/static/apple-touch-icon.png -------------------------------------------------------------------------------- /scripts/embed.go: -------------------------------------------------------------------------------- 1 | package scripts 2 | 3 | import "embed" 4 | 5 | //go:embed assets 6 | var scripts embed.FS 7 | -------------------------------------------------------------------------------- /static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/installer/instl/HEAD/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /static/android-chrome-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/installer/instl/HEAD/static/android-chrome-384x384.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Discussions 4 | url: https://github.com/installer/instl/discussions 5 | about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /templates/lib/base.gohtml: -------------------------------------------------------------------------------- 1 | {{ define "base" }} 2 | 3 | 4 | {{template "head" .}} 5 | 6 |
7 | {{template "navbar"}} 8 | {{template "content" .}} 9 |
10 | {{template "footer"}} 11 | 12 | 13 | {{end}} 14 | -------------------------------------------------------------------------------- /static/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /internal/pkg/handlers/legal.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import "github.com/gofiber/fiber/v2" 4 | 5 | func PrivacyPolicy(c *fiber.Ctx) error { 6 | return c.Render("privacy.gohtml", fiber.Map{}) 7 | } 8 | 9 | func TermsOfService(c *fiber.Ctx) error { 10 | return c.Render("tos.gohtml", fiber.Map{}) 11 | } 12 | -------------------------------------------------------------------------------- /templates/lib/footer.gohtml: -------------------------------------------------------------------------------- 1 | {{define "footer"}} 2 | 7 | {{end}} 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE specific files 2 | .idea 3 | .vscode 4 | 5 | # Compiled binaries 6 | instl.exe 7 | instl 8 | 9 | # Temporary files 10 | tmp 11 | 12 | # Test run scripts 13 | test.ps1 14 | test.sh 15 | 16 | # Metrics Database 17 | metrics.db 18 | 19 | # Experimenting folder 20 | experimenting 21 | 22 | # Testing files 23 | run.sh 24 | run.ps1 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: 🪲 Bug report 2 | description: Use this template, if you have found a bug 3 | title: '[Bug]: ' 4 | labels: 5 | - bug 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: Description 10 | description: Please describe the bug 11 | placeholder: Bug description 12 | validations: 13 | required: true 14 | -------------------------------------------------------------------------------- /scripts/assets/linux/lib/map.sh: -------------------------------------------------------------------------------- 1 | # map_put map_name key value 2 | map_put() { 3 | alias "${1}$2"="$3" 4 | } 5 | 6 | # map_get map_name key 7 | # @return value 8 | map_get() { 9 | alias "${1}$2" | awk -F"'" '{ print $2; }' 10 | } 11 | 12 | # map_keys map_name 13 | # @return map keys 14 | map_keys() { 15 | alias -p | grep $1 | cut -d'=' -f1 | awk -F"$1" '{print $2; }' 16 | } 17 | -------------------------------------------------------------------------------- /templates/lib/nav.gohtml: -------------------------------------------------------------------------------- 1 | {{define "navbar"}} 2 | 11 | {{end}} 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/proposal.yml: -------------------------------------------------------------------------------- 1 | name: ✨ Feature request 2 | description: Use this template, if you want to request a new feature 3 | title: '[Proposal]: ' 4 | labels: 5 | - proposal 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: Proposal 10 | description: Please describe your proposal 11 | placeholder: Proposal 12 | validations: 13 | required: true 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest as builder 2 | WORKDIR /build 3 | COPY ./ ./ 4 | RUN go mod download 5 | RUN CGO_ENABLED=0 go build -o ./main -v -ldflags="-X 'github.com/installer/instl/internal/pkg/global.Version=`git log -1 --pretty=format:'%h - %ci'`'" 6 | 7 | 8 | FROM alpine 9 | WORKDIR /app 10 | COPY --from=builder /build/main ./main 11 | ADD static /app/static 12 | EXPOSE 80 13 | ENTRYPOINT ["./main"] 14 | -------------------------------------------------------------------------------- /internal/pkg/platforms/platforms.go: -------------------------------------------------------------------------------- 1 | package platforms 2 | 3 | type Platform uint8 4 | 5 | func (p Platform) String() string { 6 | switch p { 7 | case Windows: 8 | return "windows" 9 | case MacOS: 10 | return "macos" 11 | case Linux: 12 | return "linux" 13 | case Unknown: 14 | return "unknown" 15 | default: 16 | return "unknown" 17 | } 18 | } 19 | 20 | const ( 21 | Unknown Platform = iota 22 | Linux 23 | MacOS 24 | Windows 25 | ) 26 | -------------------------------------------------------------------------------- /static/css/global.css: -------------------------------------------------------------------------------- 1 | /* Global CSS */ 2 | 3 | h1::before { 4 | content: "# "; 5 | color: #a8a8a8; 6 | } 7 | 8 | h2::before { 9 | content: "## "; 10 | color: #a8a8a8; 11 | } 12 | 13 | h3::before { 14 | content: "### "; 15 | color: #a8a8a8; 16 | } 17 | 18 | h4::before { 19 | content: "#### "; 20 | color: #a8a8a8; 21 | } 22 | 23 | article { 24 | padding-top: calc(1.5 * 1rem); 25 | padding-bottom: calc(1.5 * 1rem); 26 | } 27 | -------------------------------------------------------------------------------- /.github/demo.tape: -------------------------------------------------------------------------------- 1 | Output demo.gif 2 | 3 | Set FontSize 20 4 | Set Width 1200 5 | Set Height 750 6 | 7 | Hide 8 | Type "apk add curl" 9 | Enter 10 | Type "clear" 11 | Enter 12 | Sleep 2s 13 | Show 14 | 15 | # Type a command in the terminal. 16 | Type 'curl -sSL instl.sh/installer/instl-demo/linux | bash' 17 | 18 | # Pause for dramatic effect... 19 | Sleep 3s 20 | 21 | # Run the command by pressing enter. 22 | Enter 23 | 24 | # Admire the output for a bit. 25 | Sleep 20s 26 | -------------------------------------------------------------------------------- /internal/pkg/platforms/parser.go: -------------------------------------------------------------------------------- 1 | package platforms 2 | 3 | import "fmt" 4 | 5 | var platformMap = map[string]Platform{ 6 | "windows": Windows, 7 | "win": Windows, 8 | 9 | "macos": MacOS, 10 | "darwin": MacOS, 11 | "mac": MacOS, 12 | 13 | "linux": Linux, 14 | } 15 | 16 | func Parse(platform string) (Platform, error) { 17 | if p, ok := platformMap[platform]; ok { 18 | return p, nil 19 | } 20 | 21 | return 0, fmt.Errorf("unknown platform: %s", platform) 22 | } 23 | -------------------------------------------------------------------------------- /static/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "instl.sh", 3 | "short_name": "instl", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-384x384.png", 12 | "sizes": "384x384", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#000000", 17 | "background_color": "#000000" 18 | } 19 | -------------------------------------------------------------------------------- /internal/pkg/handlers/redirect.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "strings" 6 | ) 7 | 8 | func RedirectToDocs(c *fiber.Ctx) error { 9 | return c.Redirect("https://docs.instl.sh") 10 | } 11 | 12 | func RedirectLegacyStats(c *fiber.Ctx) error { 13 | // Legacy stats link: https://instl.sh/stats/user/repo 14 | // New stats link: https://instl.sh/user/repo 15 | 16 | newUrl := strings.ReplaceAll(c.OriginalURL(), "/stats", "") 17 | return c.Redirect(newUrl) 18 | } 19 | 20 | func Redirect(url string) func(c *fiber.Ctx) error { 21 | return func(c *fiber.Ctx) error { 22 | return c.Redirect(url) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /scripts/assets/shared/intro.ps1: -------------------------------------------------------------------------------- 1 | secondaryColor ' 2 | _____ _ _ _____ _______ _ 3 | |_ _| \ | |/ ____|__ __| | 4 | | | | \| | (___ | | | | 5 | | | | . ` |\___ \ | | | | 6 | _| |_| |\ |____) | | | | |____ 7 | |_____|_| \_|_____/ |_| |______|' 8 | echo "" 9 | echo "" 10 | fBlueLight ' Instl is an installer for GitHub Projects' 11 | echo "" 12 | fBlue ' > https://instl.sh' 13 | echo "" 14 | echo "" 15 | verbose "Instl version: {{ .InstlVersion }}" 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/installer/instl 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/gofiber/fiber/v2 v2.50.0 7 | go.etcd.io/bbolt v1.3.7 8 | ) 9 | 10 | require ( 11 | github.com/andybalholm/brotli v1.0.5 // indirect 12 | github.com/google/uuid v1.3.1 // indirect 13 | github.com/klauspost/compress v1.16.7 // indirect 14 | github.com/mattn/go-colorable v0.1.13 // indirect 15 | github.com/mattn/go-isatty v0.0.19 // indirect 16 | github.com/mattn/go-runewidth v0.0.15 // indirect 17 | github.com/rivo/uniseg v0.4.4 // indirect 18 | github.com/stretchr/testify v1.8.4 // indirect 19 | github.com/valyala/bytebufferpool v1.0.0 // indirect 20 | github.com/valyala/fasthttp v1.50.0 // indirect 21 | github.com/valyala/tcplisten v1.0.0 // indirect 22 | golang.org/x/sys v0.13.0 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /.github/workflows/demo-animation.yml: -------------------------------------------------------------------------------- 1 | name: Demo Animation 2 | on: 3 | push: 4 | branches: 5 | - main 6 | workflow_dispatch: 7 | schedule: 8 | - cron: "0 0 * * *" 9 | 10 | jobs: 11 | vhs: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: charmbracelet/vhs-action@v2 16 | with: 17 | path: ".github/demo.tape" 18 | - uses: stefanzweifel/git-auto-commit-action@v4 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | with: 22 | commit_message: updated demo animation 23 | branch: main 24 | commit_user_name: GitHub Actions 25 | commit_user_email: actions@github.com 26 | commit_author: GitHub Actions 27 | file_pattern: "*.gif" 28 | -------------------------------------------------------------------------------- /templates/templates.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "embed" 5 | "html/template" 6 | "io" 7 | ) 8 | 9 | //go:embed *.gohtml lib/*.gohtml 10 | var tmplFS embed.FS 11 | 12 | type Template struct { 13 | templates *template.Template 14 | } 15 | 16 | func New() *Template { 17 | templates := template.Must(template.New("").ParseFS(tmplFS, "*.gohtml", "lib/*.gohtml")) 18 | // Remove file extension from template names 19 | 20 | return &Template{ 21 | templates: templates, 22 | } 23 | } 24 | 25 | func (t *Template) Load() error { 26 | tmpl := template.Must(t.templates.Clone()) 27 | tmpl = template.Must(tmpl.ParseFS(tmplFS, "*.gohtml")) 28 | t.templates = tmpl 29 | return nil 30 | } 31 | 32 | func (t *Template) Render(w io.Writer, name string, data interface{}, s ...string) error { 33 | tmpl := template.Must(t.templates.Clone()) 34 | tmpl = template.Must(tmpl.ParseFS(tmplFS, name)) 35 | return tmpl.ExecuteTemplate(w, name, data) 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | runs-on: ${{ matrix.os }} 14 | 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, windows-latest, macOS-latest] 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v3 22 | 23 | - name: Set up Go 24 | uses: actions/setup-go@v4 25 | with: 26 | go-version: "stable" 27 | 28 | - name: Build 29 | run: go build -v . 30 | 31 | - name: Test install on Linux 32 | if: matrix.os == 'ubuntu-latest' 33 | run: make test 34 | 35 | - name: Test install on Windows 36 | if: matrix.os == 'windows-latest' 37 | run: make test-windows 38 | 39 | - name: Test install on macOS 40 | if: matrix.os == 'macOS-latest' 41 | run: make test 42 | -------------------------------------------------------------------------------- /internal/pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "time" 4 | 5 | type Config struct { 6 | Owner string 7 | Repo string 8 | Version string 9 | InstlVersion string 10 | CreatedAt time.Time 11 | Verbose bool 12 | } 13 | 14 | // Combine merges two configs together. 15 | // If a field is set in both configs, the value from the second config will be used. 16 | func Combine(configs ...Config) Config { 17 | var cfg Config 18 | for _, c := range configs { 19 | if c.Owner != "" { 20 | cfg.Owner = c.Owner 21 | } 22 | if c.Repo != "" { 23 | cfg.Repo = c.Repo 24 | } 25 | if c.Version != "" { 26 | cfg.Version = c.Version 27 | } 28 | if c.CreatedAt != (time.Time{}) { 29 | cfg.CreatedAt = c.CreatedAt 30 | } 31 | if c.Verbose { 32 | cfg.Verbose = true 33 | } 34 | if c.InstlVersion != "" { 35 | cfg.InstlVersion = c.InstlVersion 36 | } else { 37 | cfg.InstlVersion = "dev" 38 | } 39 | } 40 | return cfg 41 | } 42 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test-windows: 2 | @echo "# Testing windows installation..." 3 | @echo "## Deleting old binary..." 4 | @rm -f "$env:USERPROFILE\instl\instl-demo" 5 | @echo "## Running instl..." 6 | @go run . -test -verbose > ./run.ps1 7 | @powershell -executionpolicy bypass -File ./run.ps1 8 | @echo 9 | @echo 10 | @echo "## Testing binary..." 11 | @echo 12 | @start "$env:USERPROFILE\instl\instl-demo\instl-demo.exe" 13 | 14 | test: 15 | @echo "# Testing linux installation..." 16 | @echo "## Deleting old binary..." 17 | @rm -f "$HOME/.local/bin/instl-demo" 18 | @rm -rf "$HOME/.local/bin/.instl/instl-demo/instl-demo" 19 | @echo "## Running instl..." 20 | @go run . -test -verbose > ./run.sh 21 | @cat ./run.sh | bash 22 | @echo 23 | @echo 24 | @echo "## Testing binary..." 25 | @echo 26 | @instl-demo 27 | 28 | build: 29 | docker build -t marvinjwendt/instl . 30 | 31 | run: build 32 | docker run -it --rm -p "80:80" marvinjwendt/instl 33 | 34 | publish: build 35 | docker push marvinjwendt/instl 36 | -------------------------------------------------------------------------------- /templates/stats.gohtml: -------------------------------------------------------------------------------- 1 | {{template "base" .}} 2 | 3 | {{define "title"}}Installation statistics{{end}} 4 | 5 | {{ define "content"}} 6 |
7 |
8 |

Installation statistics

9 |

GitHub project installations via Instl: {{.Total}}

10 |
11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {{ range $index, $element := .Stats }} 23 | 24 | 25 | 26 | 27 | 28 | {{ end }} 29 | 30 |
RepositoryDownloadsGitHub Link
{{ $element.Owner }}/{{ $element.Repo }}{{ $element.Count }}GitHub
31 |
32 | {{end}} 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Marvin Wendt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/failed-installation.yml: -------------------------------------------------------------------------------- 1 | name: ❌ Failed installation 2 | description: >- 3 | Use this template if you tried to use instl to install a GitHub project, but 4 | the script failed 5 | title: '[Failed installation]: github.com/USER/REPO - ERROR MESSAGE' 6 | labels: 7 | - failed installation 8 | body: 9 | - type: input 10 | attributes: 11 | label: GitHub project 12 | description: Please link to the GitHub project that you tried to install 13 | placeholder: https://github.com/username/reponame 14 | validations: 15 | required: true 16 | - type: textarea 17 | attributes: 18 | label: Verbose log 19 | description: Please paste your verbose log output 20 | placeholder: Verbose log 21 | validations: 22 | required: true 23 | - type: markdown 24 | attributes: 25 | value: >- 26 | Get verbose output: 27 | [docs.instl.sh/usage/verbose-output](https://docs.instl.sh/usage/verbose-output) 28 | - type: textarea 29 | attributes: 30 | label: Additional info 31 | description: 'If you want to give additional context, you can do so here:' 32 | -------------------------------------------------------------------------------- /internal/pkg/handlers/badge.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "go.etcd.io/bbolt" 6 | "strconv" 7 | ) 8 | 9 | func RepoStatsShieldsIOBadge(c *fiber.Ctx) error { 10 | owner, repo := getOwnerAndRepo(c) 11 | linux, windows, macos, err := getInstallationCountPerPlatform(owner, repo) 12 | if err != nil { 13 | return err 14 | } 15 | 16 | return c.JSON(map[string]any{ 17 | "schemaVersion": 1, 18 | "label": "installations", 19 | "message": strconv.Itoa(linux + windows + macos), 20 | "color": "blue", 21 | }) 22 | } 23 | 24 | func AllStatsTotalBadge(c *fiber.Ctx) error { 25 | return db.View(func(tx *bbolt.Tx) error { 26 | b := tx.Bucket([]byte("installations")) 27 | if b == nil { 28 | return nil 29 | } 30 | var total int 31 | err := b.ForEach(func(k, v []byte) error { 32 | tmp, err := strconv.Atoi(string(v)) 33 | if err != nil { 34 | return err 35 | } 36 | total += tmp 37 | return nil 38 | }) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | return c.JSON(map[string]any{ 44 | "schemaVersion": 1, 45 | "label": "handled installations", 46 | "message": strconv.Itoa(total), 47 | "color": "blue", 48 | }) 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /templates/lib/head.gohtml: -------------------------------------------------------------------------------- 1 | {{define "head"}} 2 | 3 | 4 | 6 | 7 | 8 | {{template "title" .}} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {{end}} 23 | -------------------------------------------------------------------------------- /internal/pkg/handlers/util.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "go.etcd.io/bbolt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/gofiber/fiber/v2" 9 | ) 10 | 11 | func getOwnerAndRepo(c *fiber.Ctx) (owner, repo string) { 12 | owner = c.Params("user") 13 | owner = strings.ToLower(owner) 14 | repo = c.Params("repo") 15 | repo = strings.ToLower(repo) 16 | return owner, repo 17 | } 18 | 19 | func getInstallationCountPerPlatform(user, repo string) (linux, windows, macos int, err error) { 20 | keyBase := user + "/" + repo + "/" 21 | keyWindows := keyBase + "windows" 22 | keyLinux := keyBase + "linux" 23 | keyMacOS := keyBase + "macos" 24 | err = db.View(func(tx *bbolt.Tx) error { 25 | b := tx.Bucket([]byte("installations")) 26 | if b == nil { 27 | return nil 28 | } 29 | windowsInstallationsRaw := b.Get([]byte(keyWindows)) 30 | linuxInstallationsRaw := b.Get([]byte(keyLinux)) 31 | macosInstallationsRaw := b.Get([]byte(keyMacOS)) 32 | 33 | windowsInstallations, _ := strconv.Atoi(string(windowsInstallationsRaw)) 34 | windows = windowsInstallations 35 | linuxInstallations, _ := strconv.Atoi(string(linuxInstallationsRaw)) 36 | linux = linuxInstallations 37 | macosInstallations, _ := strconv.Atoi(string(macosInstallationsRaw)) 38 | macos = macosInstallations 39 | return nil 40 | }) 41 | if err != nil { 42 | return 0, 0, 0, err 43 | } 44 | 45 | return linux, windows, macos, nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/pkg/handlers/api.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "go.etcd.io/bbolt" 6 | "strconv" 7 | ) 8 | 9 | func RepoStatsAPI(c *fiber.Ctx) error { 10 | owner, repo := getOwnerAndRepo(c) 11 | linux, windows, macos, err := getInstallationCountPerPlatform(owner, repo) 12 | if err != nil { 13 | return err 14 | } 15 | return c.JSON(map[string]any{ 16 | "windows": windows, 17 | "linux": linux, 18 | "macos": macos, 19 | "total": linux + windows + macos, 20 | }) 21 | } 22 | 23 | func AllStatsAPI(c *fiber.Ctx) error { 24 | return db.View(func(tx *bbolt.Tx) error { 25 | b := tx.Bucket([]byte("installations")) 26 | if b == nil { 27 | return nil 28 | } 29 | stats := map[string]int{} 30 | err := b.ForEach(func(k, v []byte) error { 31 | i, err := strconv.Atoi(string(v)) 32 | if err != nil { 33 | return err 34 | } 35 | stats[string(k)] = i 36 | return nil 37 | }) 38 | if err != nil { 39 | return err 40 | } 41 | return c.JSON(stats) 42 | }) 43 | } 44 | 45 | func AllStatsTotalAPI(c *fiber.Ctx) error { 46 | return db.View(func(tx *bbolt.Tx) error { 47 | b := tx.Bucket([]byte("installations")) 48 | if b == nil { 49 | return nil 50 | } 51 | var total int 52 | err := b.ForEach(func(k, v []byte) error { 53 | tmp, err := strconv.Atoi(string(v)) 54 | if err != nil { 55 | return err 56 | } 57 | total += tmp 58 | return nil 59 | }) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | return c.SendString(strconv.Itoa(total)) 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /internal/pkg/handlers/sitemap.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gofiber/fiber/v2" 6 | "go.etcd.io/bbolt" 7 | "strings" 8 | ) 9 | 10 | func Sitemap(c *fiber.Ctx) error { 11 | var pages []string 12 | 13 | pages = append(pages, "https://instl.sh/") 14 | pages = append(pages, "https://instl.sh/stats") 15 | pages = append(pages, "https://instl.sh/privacy") 16 | pages = append(pages, "https://instl.sh/terms") 17 | 18 | var statsMap = map[string]struct{}{} 19 | err := db.View(func(tx *bbolt.Tx) error { 20 | b := tx.Bucket([]byte("installations")) 21 | if b == nil { 22 | return nil 23 | } 24 | return b.ForEach(func(k, v []byte) error { 25 | var repo string 26 | parts := strings.Split(string(k), "/") 27 | repo = strings.Join(parts[:2], "/") 28 | statsMap[repo] = struct{}{} 29 | return nil 30 | }) 31 | }) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | for repo := range statsMap { 37 | pages = append(pages, fmt.Sprintf("https://instl.sh/%s", repo)) 38 | } 39 | 40 | c.Set("Content-Type", "application/xml") 41 | return c.SendString(generateSitemap(pages)) 42 | } 43 | 44 | func generateSitemap(urls []string) string { 45 | var sitemap strings.Builder 46 | 47 | sitemap.WriteString(``) 48 | sitemap.WriteString("\n") 49 | sitemap.WriteString(``) 50 | sitemap.WriteString("\n") 51 | for _, url := range urls { 52 | sitemap.WriteString(" ") 53 | sitemap.WriteString("\n") 54 | sitemap.WriteString(fmt.Sprintf(" %s", url)) 55 | sitemap.WriteString("\n") 56 | sitemap.WriteString(" ") 57 | sitemap.WriteString("\n") 58 | } 59 | sitemap.WriteString(``) 60 | 61 | return sitemap.String() 62 | } 63 | -------------------------------------------------------------------------------- /internal/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/gofiber/fiber/v2/log" 8 | "io" 9 | "net/http" 10 | "os" 11 | ) 12 | 13 | type Metric struct { 14 | UserAgent string `json:"-"` 15 | ForwardedFor string `json:"-"` 16 | EventName string `json:"name"` 17 | URL string `json:"url"` 18 | Props map[string]string `json:"props"` 19 | Domain string `json:"domain"` 20 | } 21 | 22 | // Send sends a metric to a plausible instance 23 | func Send(m Metric) error { 24 | endpoint := os.Getenv("PLAUSIBLE_ENDPOINT") 25 | domain := os.Getenv("PLAUSIBLE_DOMAIN") 26 | if endpoint == "" { 27 | return nil 28 | } 29 | 30 | if domain == "" { 31 | return nil 32 | } else { 33 | m.Domain = domain 34 | } 35 | 36 | endpoint += "/api/event" 37 | 38 | header := map[string]string{ 39 | "Content-Type": "application/json", 40 | "User-Agent": m.UserAgent, 41 | "X-Forwarded-For": m.ForwardedFor, 42 | } 43 | 44 | if header["X-Forwarded-For"] == "" { 45 | header["X-Forwarded-For"] = "127.0.0.1" 46 | } 47 | 48 | // Post to endpoint 49 | client := &http.Client{} 50 | req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(marshal(m))) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | for key, value := range header { 56 | req.Header.Set(key, value) 57 | } 58 | 59 | resp, err := client.Do(req) 60 | defer resp.Body.Close() 61 | if err != nil { 62 | return err 63 | } 64 | 65 | response, _ := io.ReadAll(resp.Body) 66 | log.Debug(fmt.Sprintf("Sent metric to %s. Status: %s, Response: %s", endpoint, resp.Status, string(response))) 67 | 68 | return nil 69 | } 70 | 71 | func marshal(m Metric) []byte { 72 | j, _ := json.Marshal(m) 73 | return j 74 | } 75 | -------------------------------------------------------------------------------- /scripts/templates.go: -------------------------------------------------------------------------------- 1 | package scripts 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "text/template" 7 | 8 | "github.com/installer/instl/internal/pkg/config" 9 | "github.com/installer/instl/internal/pkg/platforms" 10 | ) 11 | 12 | var ( 13 | windowsTemplate *template.Template 14 | linuxTemplate *template.Template 15 | ) 16 | 17 | func ParseTemplateForPlatform(platform platforms.Platform, config config.Config) (string, error) { 18 | var tpl bytes.Buffer 19 | var t *template.Template 20 | var err error 21 | 22 | switch platform { 23 | case platforms.Windows: 24 | t, err = GetWindowsTemplate() 25 | case platforms.MacOS: 26 | t, err = GetLinuxTemplate() 27 | case platforms.Linux: 28 | t, err = GetLinuxTemplate() 29 | default: 30 | return "", errors.New("unknown platform") 31 | } 32 | if err != nil { 33 | return "", err 34 | } 35 | 36 | err = t.Execute(&tpl, config) 37 | if err != nil { 38 | return "", err 39 | } 40 | 41 | return tpl.String(), nil 42 | } 43 | 44 | func GetWindowsTemplate() (*template.Template, error) { 45 | if windowsTemplate != nil { 46 | return windowsTemplate, nil 47 | } 48 | windowsScript, err := CombineWindowsScripts() 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | tpl, err := template.New("windows").Parse(windowsScript) 54 | if err != nil { 55 | return nil, err 56 | } 57 | windowsTemplate = tpl 58 | 59 | return windowsTemplate, nil 60 | } 61 | 62 | func GetLinuxTemplate() (*template.Template, error) { 63 | if linuxTemplate != nil { 64 | return linuxTemplate, nil 65 | } 66 | linuxScript, err := CombineLinuxScripts() 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | tpl, err := template.New("linux").Parse(linuxScript) 72 | if err != nil { 73 | return nil, err 74 | } 75 | linuxTemplate = tpl 76 | 77 | return linuxTemplate, nil 78 | } 79 | -------------------------------------------------------------------------------- /scripts/combine.go: -------------------------------------------------------------------------------- 1 | package scripts 2 | 3 | import ( 4 | "path" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | // CombineLinuxScripts combines all Linux scripts into one. 10 | func CombineLinuxScripts() (string, error) { 11 | return CombineBash("assets/linux/script.sh") 12 | } 13 | 14 | // CombineWindowsScripts combines all Windows scripts into one. 15 | func CombineWindowsScripts() (string, error) { 16 | return CombinePowerShell("assets/windows/script.ps1") 17 | } 18 | 19 | // CombineBash combines multiple Bash files into one. 20 | func CombineBash(filePath string) (string, error) { 21 | return CombineFile(filePath, "source (.*)") 22 | } 23 | 24 | // CombinePowerShell combines multiple PowerShell files into one. 25 | func CombinePowerShell(filePath string) (string, error) { 26 | return CombineFile(filePath, `\. "(.*)"`) 27 | } 28 | 29 | // CombineFile combines multiple files into one. 30 | // A regex is used to find the path to the file that should be injected. 31 | func CombineFile(filePath, pattern string) (string, error) { 32 | regex := regexp.MustCompile(pattern) 33 | 34 | contentRaw, err := scripts.ReadFile(filePath) 35 | if err != nil { 36 | return "", err 37 | } 38 | content := string(contentRaw) 39 | 40 | // For each line in the file, check if it matches the pattern 41 | // If it does, replace the line with the content of the file 42 | // If it doesn't, keep the line as is 43 | lines := strings.Split(content, "\n") 44 | for i, line := range lines { 45 | if regex.MatchString(line) { 46 | foundPath := strings.TrimSpace(regex.FindStringSubmatch(line)[1]) 47 | p := path.Join(path.Dir(filePath), foundPath) 48 | contentRaw, err := scripts.ReadFile(p) 49 | if err != nil { 50 | return "", err 51 | } 52 | if strings.HasSuffix(foundPath, ".txt") { 53 | lines[i] = string(contentRaw) 54 | } else { 55 | lines[i] = "# --- Sourced from file: " + foundPath + " ---\n\n" + string(contentRaw) + "\n# --- End of " + foundPath + " ---\n\n" 56 | } 57 | } 58 | } 59 | 60 | joined := strings.Join(lines, "\n") 61 | 62 | return strings.ReplaceAll(joined, "\r\n", "\n"), nil 63 | } 64 | -------------------------------------------------------------------------------- /static/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /scripts/assets/footer.txt: -------------------------------------------------------------------------------- 1 | 2 | ################################################################################## 3 | # MIT License # 4 | # # 5 | # Copyright (c) 2022 Marvin Wendt (Instl | https://instl.sh) # 6 | # # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy # 8 | # of this software and associated documentation files (the "Software"), to deal # 9 | # in the Software without restriction, including without limitation the rights # 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # 11 | # copies of the Software, and to permit persons to whom the Software is # 12 | # furnished to do so, subject to the following conditions: # 13 | # # 14 | # The above copyright notice and this permission notice shall be included in all # 15 | # copies or substantial portions of the Software. # 16 | # # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # 23 | # SOFTWARE. # 24 | ################################################################################## 25 | 26 | # --------------------------------- Metadata ----------------------------------- # 27 | 28 | # Script generated for https://github.com/{{ .Owner }}/{{ .Repo }} 29 | # Script generated at {{ .CreatedAt }} 30 | # Script generated for version "{{ .Version }}" 31 | # Script generated with instl version "{{ .InstlVersion }}" 32 | -------------------------------------------------------------------------------- /internal/pkg/handlers/page.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "go.etcd.io/bbolt" 6 | "slices" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | func HomePage(c *fiber.Ctx) error { 12 | return c.Render("home.gohtml", nil) 13 | } 14 | 15 | func RepoStatsPage(c *fiber.Ctx) error { 16 | owner, repo := getOwnerAndRepo(c) 17 | linux, windows, macos, err := getInstallationCountPerPlatform(owner, repo) 18 | if err != nil { 19 | return err 20 | } 21 | return c.Render("repo.gohtml", map[string]any{ 22 | "Windows": windows, 23 | "Linux": linux, 24 | "MacOS": macos, 25 | "Total": linux + windows + macos, 26 | "Owner": owner, 27 | "Repo": repo, 28 | }) 29 | } 30 | 31 | func RepoMaintainerPage(c *fiber.Ctx) error { 32 | owner, repo := getOwnerAndRepo(c) 33 | linux, windows, macos, err := getInstallationCountPerPlatform(owner, repo) 34 | if err != nil { 35 | return err 36 | } 37 | return c.Render("repo-maintainer.gohtml", map[string]any{ 38 | "Windows": windows, 39 | "Linux": linux, 40 | "MacOS": macos, 41 | "Total": linux + windows + macos, 42 | "Owner": owner, 43 | "Repo": repo, 44 | }) 45 | } 46 | 47 | type Stat struct { 48 | Owner string 49 | Repo string 50 | Count int 51 | } 52 | 53 | func AllStatsPage(c *fiber.Ctx) error { 54 | statsMap := map[string]int{} 55 | 56 | err := db.View(func(tx *bbolt.Tx) error { 57 | b := tx.Bucket([]byte("installations")) 58 | if b == nil { 59 | return nil 60 | } 61 | return b.ForEach(func(k, v []byte) error { 62 | count, err := strconv.Atoi(string(v)) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | var repo string 68 | parts := strings.Split(string(k), "/") 69 | repo = strings.Join(parts[:2], "/") 70 | statsMap[repo] += count 71 | return nil 72 | }) 73 | }) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | var stats []Stat 79 | var total int 80 | 81 | for k, v := range statsMap { 82 | parts := strings.Split(k, "/") 83 | stats = append(stats, Stat{ 84 | Owner: parts[0], 85 | Repo: parts[1], 86 | Count: v, 87 | }) 88 | total += v 89 | } 90 | 91 | slices.SortFunc(stats, func(a, b Stat) int { 92 | return b.Count - a.Count 93 | }) 94 | 95 | return c.Render("stats.gohtml", map[string]any{ 96 | "Stats": stats, 97 | "Total": total, 98 | }) 99 | } 100 | -------------------------------------------------------------------------------- /templates/privacy.gohtml: -------------------------------------------------------------------------------- 1 | {{template "base" .}} 2 | 3 | {{define "title"}}instl | Privacy Policy{{end}} 4 | 5 | {{ define "content"}} 6 |
7 |
8 |

Privacy Policy

9 |

We take your privacy serious

10 |
11 |
12 |
13 |
We at instl take the privacy of our users very seriously. This Privacy Policy outlines the types of information we collect, how we use it, and the steps we take to safeguard your data.
14 | 15 |

Information We Collect

16 | 17 | 24 | 25 |

Use of Collected Information

26 | 27 |

We use the information collected for the following purposes:

28 | 29 | 34 | 35 |

Third-Party Services

36 | 37 |

We may load assets (like JavaScript files, CSS, images, etc.) from external servers. We do not control these external servers, and hence their respective privacy policies apply.

38 | 39 |

Contact Us

40 | 41 |

If you have any questions or concerns about this Privacy Policy, please contact us via GitHub.

42 | 43 |
44 | {{end}} 45 | -------------------------------------------------------------------------------- /templates/tos.gohtml: -------------------------------------------------------------------------------- 1 | {{template "base" .}} 2 | 3 | {{define "title"}}instl | Terms of Service{{end}} 4 | 5 | {{ define "content"}} 6 |
7 |
8 |

Terms of Service

9 |

By using instl you agree to the following terms

10 |
11 |
12 |
13 |
Thank you for using instl! Please read these Terms of Service ("Terms") carefully before using the instl website ("Service"). By using the Service, you agree to be bound by these Terms.
14 | 15 |

Functionality of the Service

16 |

instl provides a way to generate Bash and PowerShell scripts for the installation of GitHub repositories. The Service only performs this specific function and does not offer other features.

17 | 18 |

Eligibility

19 |

There are no age restrictions for using instl. The Service is accessible to all users.

20 | 21 |

No Registration Required

22 |

No user registration is required to use the Service.

23 | 24 |

Use of the Service

25 |

There are no restrictions on the types of projects or repositories for which scripts can be generated using instl. You are responsible for the repositories you choose to generate installation scripts for.

26 | 27 |

Free and Open Source

28 |

instl is free to use and is an open-source project. There is no paid component to the Service.

29 | 30 |

Data and Termination

31 |

You do not have an association with instl since no user registration is required. If you wish to have your repository removed from the public statistics page, contact us via GitHub.

32 | 33 |

No Warranty

34 |

The Service is provided "as is," without any warranty of any kind. We have various security measures in place, but we cannot be held liable for the generated scripts.

35 | 36 |

Dispute Resolution

37 |

If you have any disputes or issues with the Service, you may file an issue on GitHub.

38 | 39 |

Data Backup

40 |

We perform daily backups of data relevant to the Service.

41 | 42 |

Limitation of Liability

43 |

We are not liable for any damages, direct or indirect, that may arise from the use of the Service. Your use of the Service is entirely at your own risk.

44 | 45 |

Contact Us

46 |

If you have any questions about these Terms, please contact us via GitHub.

47 | 48 |
49 | {{end}} 50 | -------------------------------------------------------------------------------- /scripts/assets/linux/lib/colors.sh: -------------------------------------------------------------------------------- 1 | # Foreground Colors 2 | 3 | fRed() { 4 | printf "\e[31m%s\e[0m" "$1" 5 | } 6 | 7 | fRedLight() { 8 | printf "\e[91m%s\e[0m" "$1" 9 | } 10 | 11 | fYellow() { 12 | printf "\e[33m%s\e[0m" "$1" 13 | } 14 | 15 | fYellowLight() { 16 | printf "\e[93m%s\e[0m" "$1" 17 | } 18 | 19 | fGreen() { 20 | printf "\e[32m%s\e[0m" "$1" 21 | } 22 | 23 | fGreenLight() { 24 | printf "\e[92m%s\e[0m" "$1" 25 | } 26 | 27 | fBlue() { 28 | printf "\e[34m%s\e[0m" "$1" 29 | } 30 | 31 | fBlueLight() { 32 | printf "\e[94m%s\e[0m" "$1" 33 | } 34 | 35 | fMagenta() { 36 | printf "\e[35m%s\e[0m" "$1" 37 | } 38 | 39 | fMagentaLight() { 40 | printf "\e[95m%s\e[0m" "$1" 41 | } 42 | 43 | fCyan() { 44 | printf "\e[36m%s\e[0m" "$1" 45 | } 46 | 47 | fCyanLight() { 48 | printf "\e[96m%s\e[0m" "$1" 49 | } 50 | 51 | fWhite() { 52 | printf "\e[37m%s\e[0m" "$1" 53 | } 54 | 55 | fBlack() { 56 | printf "\e[30m%s\e[0m" "$1" 57 | } 58 | 59 | fGray() { 60 | printf "\e[90m%s\e[0m" "$1" 61 | } 62 | 63 | fGrayLight() { 64 | printf "\e[37m%s\e[0m" "$1" 65 | } 66 | 67 | ## Background Colors 68 | bRed() { 69 | printf "\e[41m%s\e[0m" "$1" 70 | } 71 | 72 | bRedLight() { 73 | printf "\e[101m%s\e[0m" "$1" 74 | } 75 | 76 | bYellow() { 77 | printf "\e[43m%s\e[0m" "$1" 78 | } 79 | 80 | bYellowLight() { 81 | printf "\e[103m%s\e[0m" "$1" 82 | } 83 | 84 | bGreen() { 85 | printf "\e[42m%s\e[0m" "$1" 86 | } 87 | 88 | bGreenLight() { 89 | printf "\e[102m%s\e[0m" "$1" 90 | } 91 | 92 | bBlue() { 93 | printf "\e[44m%s\e[0m" "$1" 94 | } 95 | 96 | bBlueLight() { 97 | printf "\e[104m%s\e[0m" "$1" 98 | } 99 | 100 | bMagenta() { 101 | printf "\e[45m%s\e[0m" "$1" 102 | } 103 | 104 | bMagentaLight() { 105 | printf "\e[105m%s\e[0m" "$1" 106 | } 107 | 108 | bCyan() { 109 | printf "\e[46m%s\e[0m" "$1" 110 | } 111 | 112 | bCyanLight() { 113 | printf "\e[106m%s\e[0m" "$1" 114 | } 115 | 116 | bWhite() { 117 | printf "\e[47m%s\e[0m" "$1" 118 | } 119 | 120 | bBlack() { 121 | printf "\e[40m%s\e[0m" "$1" 122 | } 123 | 124 | bGray() { 125 | printf "\e[100m%s\e[0m" "$1" 126 | } 127 | 128 | bGrayLight() { 129 | printf "\e[37m%s\e[0m" "$1" 130 | } 131 | 132 | 133 | # Special Colors 134 | 135 | resetColor() { 136 | printf "\e[0m" 137 | } 138 | 139 | # Theme 140 | 141 | primaryColor() { 142 | fCyan "$1" 143 | } 144 | 145 | secondaryColor() { 146 | fMagentaLight "$1" 147 | } 148 | 149 | info() { 150 | fBlueLight " i " && resetColor && fBlue "$1" && echo 151 | } 152 | 153 | warning() { 154 | fYellowLight " ! " && resetColor && fYellow "$1" && echo 155 | } 156 | 157 | error() { 158 | fRedLight " X " && resetColor && fRed "$1" && echo 159 | exit 1 160 | } 161 | 162 | success() { 163 | fGreenLight " + " && resetColor && fGreen "$1" && echo 164 | } 165 | 166 | verbose() { 167 | if [ $verbose == true ]; then 168 | fGrayLight " > " && resetColor && fGray "$1" && echo 169 | fi 170 | } 171 | -------------------------------------------------------------------------------- /internal/pkg/handlers/installation.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gofiber/fiber/v2/log" 6 | "github.com/installer/instl/internal/metrics" 7 | "github.com/installer/instl/internal/pkg/global" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/gofiber/fiber/v2" 12 | "go.etcd.io/bbolt" 13 | 14 | "github.com/installer/instl/internal/pkg/config" 15 | "github.com/installer/instl/internal/pkg/platforms" 16 | "github.com/installer/instl/scripts" 17 | ) 18 | 19 | var db *bbolt.DB 20 | 21 | func init() { 22 | dbTmp, err := bbolt.Open("./metrics.db", 0666, nil) 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | db = dbTmp 28 | } 29 | 30 | func installationWithConfig(cfg config.Config) func(c *fiber.Ctx) error { 31 | return func(c *fiber.Ctx) error { 32 | owner, repo := getOwnerAndRepo(c) 33 | platform, err := platforms.Parse(c.Params("os")) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | script, err := scripts.ParseTemplateForPlatform(platform, config.Combine( 39 | config.Config{ 40 | Owner: owner, 41 | Repo: repo, 42 | CreatedAt: time.Now(), 43 | Version: "latest", 44 | InstlVersion: global.Version, 45 | }, 46 | cfg, 47 | )) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | err = c.SendString(script) 53 | 54 | key := owner + "/" + repo + "/" + platform.String() 55 | if owner != "status-health-check" { 56 | // Send metrics 57 | m := metrics.Metric{ 58 | UserAgent: c.Get("User-Agent"), 59 | ForwardedFor: c.Get("X-Forwarded-For"), 60 | EventName: "installation", 61 | URL: fmt.Sprintf("https://instl.sh/%s/%s/%s", owner, repo, platform), 62 | Props: map[string]string{ 63 | "platform": platform.String(), 64 | "repo": owner + "/" + repo, 65 | }, 66 | } 67 | 68 | go func() { 69 | err := metrics.Send(m) 70 | if err != nil { 71 | log.Errorw("failed to send metrics", "error", err) 72 | } 73 | }() 74 | 75 | // Don't return error, user is not affected by errors that happen in stats collection 76 | db.Update(func(tx *bbolt.Tx) error { 77 | b, err := tx.CreateBucketIfNotExists([]byte("installations")) 78 | if err != nil { 79 | return err 80 | } 81 | var installCountString string 82 | if v := b.Get([]byte(key)); v != nil { 83 | installCountString = string(v) 84 | } 85 | var installCount int 86 | if installCountString != "" { 87 | installCount, err = strconv.Atoi(installCountString) 88 | if err != nil { 89 | return err 90 | } 91 | } 92 | installCount++ 93 | return b.Put([]byte(key), []byte(strconv.Itoa(installCount))) 94 | }) 95 | } 96 | 97 | return err 98 | } 99 | } 100 | 101 | func Installation(c *fiber.Ctx) error { 102 | return installationWithConfig(config.Config{})(c) 103 | } 104 | 105 | func InstallationVerbose(c *fiber.Ctx) error { 106 | return installationWithConfig(config.Config{Verbose: true})(c) 107 | } 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

INSTL
The Easiest Installer for GitHub Projects

2 | 3 |

4 | 5 | 6 | Downloads 7 | 8 | 9 | 10 | Handled installations 11 | 12 | 13 |

14 | 15 |

16 | Instl Demo 17 |

18 | 19 | ---- 20 | 21 |

22 | Documentation 23 | | 24 | Contributing 25 |

26 | 27 | ---- 28 | 29 |

30 | Instl is an installation script generator for GitHub projects.
31 | It does not need any setup, and can be used to install most GitHub projects on Linux, macOS and Windows.
32 | You can easily add installation commands to your README.md - they just work! 33 |

34 | 35 | ## Key Features 36 | 37 | - 💻 Cross-Platform: Works on Windows, macOS and Linux out of the box 38 | - 🧸 One-Click Installs: Install any GitHub project with just a single command 39 | - 🪶 Web-Based: No need to install Instl - scripts are generated server-side 40 | - ⚙️ Intelligent Configuration: Instl adapts to the project's structure 41 | - 🕊️ On the fly: Installer scripts are created in real-time for each project 42 | - 📊 [Track Your Installs](https://instl.sh/stats): Installation metrics for your projects at your fingertips 43 | 44 | ## Try it out: 45 | 46 | Install our demo repository, `instl-demo`, to see instl in action. If successful, you should be able to run `instl-demo` right from your terminal. 47 | 48 | | Platform | Command | 49 | | -------- |------------------------------------------------------------| 50 | | Windows | iwr instl.sh/installer/instl-demo/windows \| iex | 51 | | macOS | curl -sSL instl.sh/installer/instl-demo/macos \| bash | 52 | | Linux | curl -sSL instl.sh/installer/instl-demo/linux \| bash | 53 | 54 | 55 | ## Usage 56 | 57 | The fastest way to create your own instl commands, is by visiting [instl.sh](https://instl.sh) and using the builder. 58 | 59 | Alternatively, you can create your own commands by using the following URL structure: 60 | 61 | > [!NOTE] 62 | > Replace `{username}` and `{reponame}` with your GitHub username and repository name. 63 | 64 | 65 | #### Windows 66 | 67 | ```powershell 68 | iwr instl.sh/{username}/{reponame}/windows | iex 69 | ``` 70 | 71 | #### macOS 72 | 73 | ```bash 74 | curl -sSL instl.sh/{username}/{reponame}/macos | bash 75 | ``` 76 | 77 | #### Linux 78 | 79 | ```bash 80 | curl -sSL instl.sh/{username}/{reponame}/linux | bash 81 | ``` 82 | -------------------------------------------------------------------------------- /scripts/assets/header.txt: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # # 3 | # _____ _ _ _____ _______ _ # 4 | # |_ _| \ | |/ ____|__ __| | # 5 | # | | | \| | (___ | | | | # 6 | # | | | . ` |\___ \ | | | | # 7 | # _| |_| |\ |____) | | | | |____ # 8 | # |_____|_| \_|_____/ |_| |______| # 9 | # # 10 | # # 11 | # Instl is an automated installer for GitHub Projects. # 12 | # This script is automatically generated by https://instl.sh. # 13 | # # 14 | # Running this script will install the specified GitHub project on your # 15 | # system. # 16 | # # 17 | # For full transperancy this script is a single file, combined from multiple # 18 | # files. # 19 | # # 20 | # You can see the source code of this server on GitHub: # 21 | # https://github.com/installer/instl # 22 | # # 23 | # Instl and its members are not associated with the # 24 | # project that will be installed! # 25 | # # 26 | # # 27 | # This script will perform the following actions: # 28 | # - Fetch the project metadata from the public GitHub API # 29 | # - Find the right asset for your system # 30 | # - Download the asset from GitHub # 31 | # - Extract the asset to the correct location # 32 | # - Add the installation directory to your system PATH # 33 | ################################################################################ 34 | 35 | # Install script for: https://github.com/{{ .Owner }}/{{ .Repo }} 36 | # 37 | # To use this install script, run the following command: 38 | # 39 | # Linux: 40 | # curl -sSL instl.sh/{{ .Owner }}/{{ .Repo }}/linux | bash 41 | # 42 | # macOS: 43 | # curl -sSL instl.sh/{{ .Owner }}/{{ .Repo }}/macos | bash 44 | # 45 | # Windows: 46 | # iwr instl.sh/{{ .Owner }}/{{ .Repo }}/windows | iex 47 | 48 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= 2 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/gofiber/fiber/v2 v2.50.0 h1:ia0JaB+uw3GpNSCR5nvC5dsaxXjRU5OEu36aytx+zGw= 6 | github.com/gofiber/fiber/v2 v2.50.0/go.mod h1:21eytvay9Is7S6z+OgPi7c7n4++tnClWmhpimVHMimw= 7 | github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= 8 | github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 9 | github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= 10 | github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 11 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 12 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 13 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 14 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 15 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 16 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 17 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 21 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= 22 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 23 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 24 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 25 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 26 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 27 | github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M= 28 | github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= 29 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 30 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 31 | go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= 32 | go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= 33 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 34 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 35 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 36 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 37 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 38 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 39 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/gofiber/fiber/v2/log" 7 | "github.com/gofiber/fiber/v2/middleware/logger" 8 | "github.com/installer/instl/internal/pkg/global" 9 | "github.com/installer/instl/templates" 10 | "os" 11 | "runtime" 12 | "time" 13 | 14 | "github.com/gofiber/fiber/v2" 15 | "github.com/gofiber/fiber/v2/middleware/cors" 16 | "github.com/gofiber/fiber/v2/middleware/recover" 17 | "github.com/installer/instl/internal/pkg/config" 18 | "github.com/installer/instl/internal/pkg/handlers" 19 | "github.com/installer/instl/internal/pkg/platforms" 20 | "github.com/installer/instl/scripts" 21 | ) 22 | 23 | func main() { 24 | test := flag.Bool("test", false, "enable test mode; don't start server; print script to stdout") 25 | verbose := flag.Bool("verbose", false, "verbose output for test mode") 26 | owner := flag.String("owner", "installer", "repo owner for test mode") 27 | repo := flag.String("repo", "instl-demo", "repo name for test mode") 28 | flag.Parse() 29 | 30 | // Check if test flag is set 31 | if *test { 32 | platform, err := platforms.Parse(runtime.GOOS) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | script, err := scripts.ParseTemplateForPlatform(platform, config.Config{ 38 | Owner: *owner, 39 | Repo: *repo, 40 | Version: "latest", 41 | CreatedAt: time.Now(), 42 | Verbose: *verbose, 43 | InstlVersion: global.Version, 44 | }) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | 49 | fmt.Println(script) 50 | os.Exit(0) 51 | } 52 | 53 | fmt.Println(` ██ ███ ██ ███████ ████████ ██ 54 | ██ ████ ██ ██ ██ ██ 55 | ██ ██ ██ ██ ███████ ██ ██ 56 | ██ ██ ██ ██ ██ ██ ██ 57 | ██ ██ ████ ███████ ██ ███████ 58 | `) 59 | 60 | engine := templates.New() 61 | app := fiber.New(fiber.Config{ 62 | DisableStartupMessage: true, 63 | Views: engine, 64 | }) 65 | 66 | app.Use(logger.New()) 67 | app.Use(recover.New()) 68 | app.Use(cors.New(cors.Config{ 69 | AllowOrigins: "*", 70 | })) 71 | 72 | app.Get("/", handlers.HomePage) 73 | app.Static("/", "./static") 74 | app.Get("/docs", handlers.RedirectToDocs) 75 | app.Get("/documentation", handlers.RedirectToDocs) 76 | 77 | // Legal 78 | app.Get("/privacy", handlers.PrivacyPolicy) 79 | app.Get("/terms", handlers.TermsOfService) 80 | app.Get("/tos", handlers.Redirect("/terms")) 81 | 82 | // Sitemap 83 | app.Get("/sitemap.xml", handlers.Sitemap) 84 | 85 | // API 86 | apiV1 := app.Group("/api/v1") 87 | apiV1.Get("/stats", handlers.AllStatsAPI) 88 | apiV1.Get("/stats/:user/:repo", handlers.RepoStatsAPI) 89 | apiV1.Get("/stats/total", handlers.AllStatsTotalAPI) 90 | // - Badge 91 | badgeAPIV1 := apiV1.Group("/badge") 92 | shieldsIOAPIV1 := badgeAPIV1.Group("/shields.io") 93 | shieldsIOAPIV1.Get("/stats/:user/:repo", handlers.RepoStatsShieldsIOBadge) 94 | shieldsIOAPIV1.Get("/stats/total", handlers.AllStatsTotalBadge) 95 | 96 | // Stats 97 | app.Get("/stats", handlers.AllStatsPage) 98 | app.Get("/stats/:any", func(ctx *fiber.Ctx) error { return ctx.SendStatus(404) }) 99 | app.Get("/stats/:user/:repo", handlers.RedirectLegacyStats) // Legacy redirect 100 | app.Get("/:user/:repo", handlers.RepoStatsPage) 101 | app.Get("/:user/:repo/maintainer", handlers.RepoMaintainerPage) 102 | 103 | // Installation script generator 104 | app.Get("/:user/:repo/:os", handlers.Installation) 105 | app.Get("/:user/:repo/:os/verbose", handlers.InstallationVerbose) 106 | 107 | log.Fatal(app.Listen(":80")) 108 | } 109 | -------------------------------------------------------------------------------- /templates/repo.gohtml: -------------------------------------------------------------------------------- 1 | {{template "base" .}} 2 | 3 | {{define "title"}}Installer for {{.Owner}}/{{.Repo}}{{end}} 4 | 5 | {{ define "content"}} 6 |
7 |
8 |

Installer for github.com/{{.Owner}}/{{.Repo}}

9 |

10 |
11 |
12 |
13 |
14 |

Install

15 |

Install {{.Owner}}/{{.Repo}} by running the right 16 | command for your platform

17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
PlatformCommand
Windowsiwr instl.sh/{{.Owner}}/{{.Repo}}/windows | iex
Linuxcurl -sSL instl.sh/{{.Owner}}/{{.Repo}}/linux | bash
macOScurl -sSL instl.sh/{{.Owner}}/{{.Repo}}/macos | bash
40 | 41 |

📊 Statistics

42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
Total downloads{{ .Total }}
Linux downloads{{ .Linux }}
macOS downloads{{ .MacOS }}
Windows downloads{{ .Windows }}
63 | 64 |
65 |

Maintainer

66 |

Maintaining {{.Owner}}/{{.Repo}}?

67 |
68 |

69 | If you are a maintainer of github.com/{{.Owner}}/{{.Repo}}, 70 | you can check out the maintainer page of this repo for 71 | customized badges and Markdown snippets. 72 |

73 | 74 | 75 |
76 | 77 | 104 | {{end}} 105 | -------------------------------------------------------------------------------- /templates/home.gohtml: -------------------------------------------------------------------------------- 1 | {{template "base"}} 2 | 3 | {{define "title"}}instl: The Easiest Installer for GitHub Projects{{end}} 4 | 5 | {{define "content"}} 6 |
7 |
8 |

Instl: The Easiest Installer for GitHub Projects

9 |

No need for configuration - it just works!

10 |
11 |
12 | 13 |
14 |
15 | Instl is an installation script generator for GitHub projects. It does not need any setup, and can be used to install most GitHub projects on Linux, macOS and Windows.
16 | You can easily add installation commands to your README - they just work! 17 |
18 |
19 |
20 | 24 | 28 |
29 | 30 | 31 |
32 | 33 |

Key Features

34 | 43 | 44 |
45 |

Try it out:

46 |

Ready for a spin?

47 |
48 |

49 | Install our demo repository, instl-demo, to see Instl in action. 50 | If successful, you should be able to run instl-demo right from your terminal. 51 |

52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
PlatformCommand
Windowsiwr instl.sh/installer/instl-demo/windows | iex
Linuxcurl -sSL instl.sh/installer/instl-demo/linux | bash
macOScurl -sSL instl.sh/installer/instl-demo/macos | bash
74 | 75 |

76 | Don't want to install our demo repo, but you still want to see Instl in action? No problem! 77 |

78 |

79 | demo animation 80 |

81 |
82 | 83 | 98 | {{end}} 99 | -------------------------------------------------------------------------------- /templates/repo-maintainer.gohtml: -------------------------------------------------------------------------------- 1 | {{template "base" .}} 2 | 3 | {{define "title"}}Maintainer view for {{.Owner}}/{{.Repo}}{{end}} 4 | 5 | {{ define "content"}} 6 |
7 |
8 |

9 | Maintainer view for 10 | github.com/{{.Owner}}/{{.Repo}} 11 |

12 |

User view

13 |
14 |
15 |
16 |
17 | This is the maintainer view for 18 | github.com/{{.Owner}}/{{.Repo}}. 19 | If you are not a maintainer of the repository, you might find the 20 | user view more interesting. 21 |
22 | 23 |

Badges

24 | 25 | 31 | 32 |
33 |

Install with instl.sh

34 |

35 | 36 | install with instl.sh 38 | 39 |

40 |
41 |
[![install with instl.sh](https://img.shields.io/badge/install_with-instl.sh-blue?link=https://instl.sh/{{.Owner}}/{{.Repo}}&style=for-the-badge)](https://instl.sh/{{.Owner}}/{{.Repo}})
42 | 43 |
44 |

Installation count

45 |

46 | 47 | install count 49 | 50 |

51 |
[![install count](https://img.shields.io/endpoint?url=https://instl.sh/api/v1/badge/shields.io/stats/{{.Owner}}/{{.Repo}}&style=for-the-badge)](https://instl.sh/{{.Owner}}/{{.Repo}})
52 | 53 |

Installation Commands

54 | 55 |

Table

56 |

 57 | | Platform | Command |
 58 | | -------- | ------- |
 59 | | Windows  | <code>iwr instl.sh/{{.Owner}}/{{.Repo}}/windows | iex<code>      |
 60 | | macOS    | <code>curl -sSL instl.sh/{{.Owner}}/{{.Repo}}/macos | bash<code> |
 61 | | Linux    | <code>curl -sSL instl.sh/{{.Owner}}/{{.Repo}}/linux | bash<code> |
 62 | 
 63 | > [📊 Install stats](https://instl.sh/{{.Owner}}/{{.Repo}})
 64 | 
65 | 66 |

Headings

67 |

 68 | #### Windows
 69 | ```powershell
 70 | iwr instl.sh/{{.Owner}}/{{.Repo}}/windows | iex
 71 | ```
 72 | 
 73 | #### macOS
 74 | ```bash
 75 | curl -sSL instl.sh/{{.Owner}}/{{.Repo}}/macos | bash
 76 | ```
 77 | 
 78 | #### Linux
 79 | ```bash
 80 | curl -sSL instl.sh/{{.Owner}}/{{.Repo}}/linux | bash
 81 | ```
 82 | 
 83 | > [📊 Install stats](https://instl.sh/{{.Owner}}/{{.Repo}})
 84 | 
85 | 86 |
87 | 88 | 121 | {{end}} 122 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | the provided contact methods. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /scripts/assets/windows/lib/colors.ps1: -------------------------------------------------------------------------------- 1 | # Foreground Colors 2 | 3 | function fRed 4 | { 5 | param ( 6 | $Msg 7 | ) 8 | Write-Host $Msg -NoNewline -ForegroundColor DarkRed 9 | } 10 | 11 | function fRedLight 12 | { 13 | param ( 14 | $Msg 15 | ) 16 | Write-Host $Msg -NoNewline -ForegroundColor Red 17 | } 18 | 19 | function fGreen 20 | { 21 | param ( 22 | $Msg 23 | ) 24 | Write-Host $Msg -NoNewline -ForegroundColor DarkGreen 25 | } 26 | 27 | function fGreenLight 28 | { 29 | param ( 30 | $Msg 31 | ) 32 | Write-Host $Msg -NoNewline -ForegroundColor Green 33 | } 34 | 35 | function fYellow 36 | { 37 | param ( 38 | $Msg 39 | ) 40 | Write-Host $Msg -NoNewline -ForegroundColor DarkYellow 41 | } 42 | 43 | function fYellowLight 44 | { 45 | param ( 46 | $Msg 47 | ) 48 | Write-Host $Msg -NoNewline -ForegroundColor Yellow 49 | } 50 | 51 | function fBlue 52 | { 53 | param ( 54 | $Msg 55 | ) 56 | Write-Host $Msg -NoNewline -ForegroundColor DarkBlue 57 | } 58 | 59 | function fBlueLight 60 | { 61 | param ( 62 | $Msg 63 | ) 64 | Write-Host $Msg -NoNewline -ForegroundColor Blue 65 | } 66 | 67 | function fMagenta 68 | { 69 | param ( 70 | $Msg 71 | ) 72 | Write-Host $Msg -NoNewline -ForegroundColor DarkMagenta 73 | } 74 | 75 | function fMagentaLight 76 | { 77 | param ( 78 | $Msg 79 | ) 80 | Write-Host $Msg -NoNewline -ForegroundColor Magenta 81 | } 82 | 83 | function fCyan 84 | { 85 | param ( 86 | $Msg 87 | ) 88 | Write-Host $Msg -NoNewline -ForegroundColor DarkCyan 89 | } 90 | 91 | function fCyanLight 92 | { 93 | param ( 94 | $Msg 95 | ) 96 | Write-Host $Msg -NoNewline -ForegroundColor Cyan 97 | } 98 | 99 | function fWhite 100 | { 101 | param ( 102 | $Msg 103 | ) 104 | Write-Host $Msg -NoNewline -ForegroundColor White 105 | } 106 | 107 | function fBlack 108 | { 109 | param ( 110 | $Msg 111 | ) 112 | Write-Host $Msg -NoNewline -ForegroundColor Black 113 | } 114 | 115 | function fGray 116 | { 117 | param ( 118 | $Msg 119 | ) 120 | Write-Host $Msg -NoNewline -ForegroundColor DarkGray 121 | } 122 | 123 | function fGrayLight 124 | { 125 | param ( 126 | $Msg 127 | ) 128 | Write-Host $Msg -NoNewline -ForegroundColor Gray 129 | } 130 | 131 | # Background Colors 132 | 133 | function bRed 134 | { 135 | param ( 136 | $Msg 137 | ) 138 | Write-Host $Msg -NoNewline -BackgroundColor DarkRed 139 | } 140 | 141 | function bRedLight 142 | { 143 | param ( 144 | $Msg 145 | ) 146 | Write-Host $Msg -NoNewline -BackgroundColor Red 147 | } 148 | 149 | function bGreen 150 | { 151 | param ( 152 | $Msg 153 | ) 154 | Write-Host $Msg -NoNewline -BackgroundColor DarkGreen 155 | } 156 | 157 | function bGreenLight 158 | { 159 | param ( 160 | $Msg 161 | ) 162 | Write-Host $Msg -NoNewline -BackgroundColor Green 163 | } 164 | 165 | function bYellow 166 | { 167 | param ( 168 | $Msg 169 | ) 170 | Write-Host $Msg -NoNewline -BackgroundColor DarkYellow 171 | } 172 | 173 | function bYellowLight 174 | { 175 | param ( 176 | $Msg 177 | ) 178 | Write-Host $Msg -NoNewline -BackgroundColor Yellow 179 | } 180 | 181 | function bBlue 182 | { 183 | param ( 184 | $Msg 185 | ) 186 | Write-Host $Msg -NoNewline -BackgroundColor DarkBlue 187 | } 188 | 189 | function bBlueLight 190 | { 191 | param ( 192 | $Msg 193 | ) 194 | Write-Host $Msg -NoNewline -BackgroundColor Blue 195 | } 196 | 197 | function bMagenta 198 | { 199 | param ( 200 | $Msg 201 | ) 202 | Write-Host $Msg -NoNewline -BackgroundColor DarkMagenta 203 | } 204 | 205 | function bMagentaLight 206 | { 207 | param ( 208 | $Msg 209 | ) 210 | Write-Host $Msg -NoNewline -BackgroundColor Magenta 211 | } 212 | 213 | function bCyan 214 | { 215 | param ( 216 | $Msg 217 | ) 218 | Write-Host $Msg -NoNewline -BackgroundColor DarkCyan 219 | } 220 | 221 | function bCyanLight 222 | { 223 | param ( 224 | $Msg 225 | ) 226 | Write-Host $Msg -NoNewline -BackgroundColor Cyan 227 | } 228 | 229 | function bWhite 230 | { 231 | param ( 232 | $Msg 233 | ) 234 | Write-Host $Msg -NoNewline -BackgroundColor White 235 | } 236 | 237 | function bBlack 238 | { 239 | param ( 240 | $Msg 241 | ) 242 | Write-Host $Msg -NoNewline -BackgroundColor Black 243 | } 244 | 245 | function bGray 246 | { 247 | param ( 248 | $Msg 249 | ) 250 | Write-Host $Msg -NoNewline -BackgroundColor DarkGray 251 | } 252 | 253 | function bGrayLight 254 | { 255 | param ( 256 | $Msg 257 | ) 258 | Write-Host $Msg -NoNewline -BackgroundColor Gray 259 | } 260 | 261 | # Special Colors 262 | 263 | function resetColor 264 | { 265 | Write-Host -NoNewline -Reset 266 | } 267 | 268 | # Theme 269 | 270 | function primaryColor 271 | { 272 | param ( 273 | $Msg 274 | ) 275 | fCyan $Msg 276 | } 277 | 278 | function secondaryColor 279 | { 280 | param ( 281 | $Msg 282 | ) 283 | fMagentaLight $Msg 284 | } 285 | 286 | function info 287 | { 288 | param ( 289 | $Msg 290 | ) 291 | fBlueLight " i " 292 | fBlue $Msg 293 | echo "" 294 | } 295 | 296 | function warning 297 | { 298 | param ( 299 | $Msg 300 | ) 301 | fYellowLight " ! " 302 | fYellow $Msg 303 | echo "" 304 | } 305 | 306 | function error 307 | { 308 | param ( 309 | $Msg 310 | ) 311 | fRedLight " X " 312 | fRed $Msg 313 | echo "" 314 | exit 1 315 | } 316 | 317 | function success 318 | { 319 | param ( 320 | $Msg 321 | ) 322 | fGreenLight " + " 323 | fGreen $Msg 324 | echo "" 325 | } 326 | 327 | function verbose 328 | { 329 | param ( 330 | $Msg 331 | ) 332 | if ($verbose) 333 | { 334 | fGrayLight " > " 335 | fGray $Msg 336 | echo "" 337 | } 338 | } 339 | 340 | -------------------------------------------------------------------------------- /scripts/assets/windows/script.ps1: -------------------------------------------------------------------------------- 1 | . "../header.txt" 2 | 3 | # Import libraries 4 | . "./lib/colors.ps1" 5 | 6 | # Setup variables 7 | $verboseString = "{{ .Verbose }}" 8 | $verbose = $false 9 | if ($verboseString -eq "true") 10 | { 11 | $verbose = $true 12 | } 13 | 14 | verbose "Setting up variables" 15 | $owner = "{{ .Owner }}" 16 | $repo = "{{ .Repo }}" 17 | 18 | $tmpDir = "$env:TEMP\instl-cache" 19 | verbose "Temporary directory: $tmpDir" 20 | # Remove previous cache, if it exists 21 | if (test-path $tmpDir) 22 | { 23 | rm -r -fo $tmpDir 24 | } 25 | New-Item -Path $tmpDir -ItemType Directory > $null 26 | 27 | $installLocation = "$HOME\instl\$repo" 28 | verbose "Install location: $installLocation" 29 | # Remove previous installation, if it exists 30 | if (test-path $installLocation) 31 | { 32 | Remove-Item -r -fo $installLocation 33 | } 34 | New-Item -Path $installLocation -ItemType Directory > $null 35 | 36 | # Print "INSTL" header 37 | . "../shared/intro.ps1" 38 | 39 | # Installation 40 | $headers = @{ 41 | 'user-agent' = 'instl' 42 | } 43 | 44 | # Check for GH_TOKEN in environment variables 45 | if ($env:GH_TOKEN) { 46 | verbose "Using authentication with GH_TOKEN" 47 | $Headers['Authorization'] = "Bearer $($env:GH_TOKEN)" 48 | } 49 | # Check for GITHUB_TOKEN in environment variables 50 | elseif ($env:GITHUB_TOKEN) { 51 | verbose "Using authentication with GITHUB_TOKEN" 52 | $Headers['Authorization'] = "Bearer $($env:GITHUB_TOKEN)" 53 | } 54 | # No authentication tokens found 55 | else { 56 | verbose "No authentication" 57 | } 58 | 59 | # GitHub public API 60 | $latestReleaseURL = "https://api.github.com/repos/$owner/$repo/releases/latest" 61 | $latestRelease = Invoke-WebRequest -Method Get -Uri $latestReleaseURL -Headers $Headers | ConvertFrom-Json 62 | $tagName = $latestRelease.tag_name 63 | info "Found latest release of $repo (version: $tagName)" 64 | 65 | # Get list of assets 66 | verbose "Getting list of assets" 67 | $assetsRaw = $latestRelease.assets 68 | # Map to array 69 | $assets = $assetsRaw | ForEach-Object { $_.browser_download_url } 70 | $assetCount = $assets.Count 71 | info "Found $assetCount assets in '$tagName' - searching for one that fits your system..." 72 | 73 | # Get host architecture 74 | $arch = $env:PROCESSOR_ARCHITECTURE 75 | # arch to lowercase 76 | $arch = $arch.ToLower() 77 | verbose "Host architecture: $arch" 78 | 79 | # Set aliases for architecture 80 | $amd64 = @("amd64", "x86_64", "x86-64", "x64") 81 | $amd32 = @("386", "i386", "i686", "x86") 82 | $windows = @("windows", "win") 83 | 84 | $currentArchAliases = @() 85 | if ($arch -eq "amd64") 86 | { 87 | $currentArchAliases = $amd64 88 | } 89 | elseif ($arch -eq "x86") 90 | { 91 | $currentArchAliases = $amd32 92 | } 93 | else 94 | { 95 | error "Unsupported architecture: $arch" 96 | } 97 | verbose "Current architecture aliases: $currentArchAliases" 98 | 99 | # Create hastable of assets and a score 100 | $assetMap = @{ } 101 | # Loop through assets 102 | foreach ($asset in $assets) 103 | { 104 | if ( $asset.ToLower().Contains("darwin")) 105 | { 106 | continue 107 | } 108 | if ( $asset.ToLower().EndsWith(".sbom")) 109 | { 110 | continue 111 | } 112 | $assetMap[$asset] = 0 113 | # Loop through windows aliases 114 | $windows | %{ 115 | $windowsAlias = $_ 116 | # If asset contains architecture alias, increase score 117 | if ( $asset.ToLower().Contains($windowsAlias)) 118 | { 119 | verbose "Asset $asset contains windows alias $windowsAlias" 120 | $assetMap[$asset] = $assetMap[$asset] + 1 121 | } 122 | } 123 | 124 | # Loop through architecture aliases 125 | $currentArchAliases | %{ 126 | $archAlias = $_ 127 | # If asset contains architecture alias, increase score 128 | if ( $asset.ToLower().Contains($archAlias)) 129 | { 130 | verbose "Asset $asset contains architecture alias $archAlias" 131 | $assetMap[$asset] = $assetMap[$asset] + 1 132 | } 133 | } 134 | } 135 | 136 | # Get highest score 137 | $highestScore = 0 138 | $highestScoreAsset = "" 139 | foreach ($Key in $assetMap.Keys) 140 | { 141 | $asset = $Key 142 | $score = $assetMap[$Key] 143 | verbose "Asset: $asset, score: $score" 144 | if ($score -gt $highestScore) 145 | { 146 | $highestScore = $score 147 | $highestScoreAsset = $asset 148 | } 149 | } 150 | 151 | # Check if no asset is found 152 | if ($highestScore -eq 0) 153 | { 154 | error "Could not find any assets that fit your system" 155 | } 156 | 157 | $assetURL = $highestScoreAsset 158 | $assetName = $assetURL.Split('/')[-1] 159 | info "Found asset with highest match score: $assetName" 160 | 161 | # Downoad asset 162 | info "Downloading asset..." 163 | $assetPath = "$tmpDir\$assetName" 164 | Invoke-WebRequest -Uri $assetURL -OutFile $assetPath -Headers $Headers 165 | verbose "Asset downloaded to $assetPath" 166 | 167 | info "Installing $repo" 168 | # Extract asset if it is a zip file 169 | if ( $assetName.EndsWith(".zip")) 170 | { 171 | verbose "Extracting asset..." 172 | Expand-Archive -Path $assetPath -Destination $installLocation\ 173 | verbose "Asset extracted to $installLocation" 174 | } 175 | elseif ( $assetName.EndsWith(".tar.gz")) 176 | { 177 | verbose "Extracting asset..." 178 | tar -xzf $assetPath -C $installLocation 179 | verbose "Asset extracted to $installLocation" 180 | } 181 | elseif ( $assetName.EndsWith(".tar")) 182 | { 183 | verbose "Extracting asset..." 184 | tar -xf $assetPath -C $installLocation 185 | verbose "Asset extracted to $installLocation" 186 | } 187 | else 188 | { 189 | error "Asset is not a zip, tar or tar.gz file" 190 | } 191 | 192 | # If it was unpacked to a single directory, move the files to the root of the tmpDir 193 | # Also check that there are not other non directory files in the tmpDir 194 | verbose "Checking if asset was unpacked to a single directory" 195 | $files = Get-ChildItem -Path $installLocation 196 | if ($files.Count -eq 1 -and $files[0].PSIsContainer) 197 | { 198 | verbose "Moving files to root of tmpDir" 199 | $subDir = $files[0] 200 | $subDirPath = $subDir.FullName 201 | $subDirFiles = Get-ChildItem -Path $subDirPath 202 | foreach ($file in $subDirFiles) 203 | { 204 | $filePath = $file.FullName 205 | $fileName = $file.Name 206 | verbose "Moving $fileName to root of tmpDir" 207 | Move-Item -Path $filePath -Destination $installLocation 208 | } 209 | } 210 | else 211 | { 212 | verbose "Asset was not unpacked to a single directory" 213 | } 214 | 215 | # Find binary file in install path 216 | $binaryFile = (Get-ChildItem -Path $installLocation -Filter "*.exe")[0] 217 | $binaryFile = $installLocation + "\" + $binaryFile.Name 218 | $binaryName = $binaryFile.Split('\')[-1] 219 | $command = $binaryName.Split('.')[0] 220 | verbose "Binary file: $binaryFile" 221 | 222 | # Change PATH to include install location 223 | verbose "Changing PATH to include install location" 224 | $oldPath = (Get-ItemProperty -Path 'Registry::HKEY_CURRENT_USER\Environment' -Name PATH).path 225 | # if oldPath does not contain install location, add it 226 | if (!$oldPath.Contains($installLocation)) 227 | { 228 | verbose "PATH does not contain install location, adding it" 229 | $newPath = $oldPath + ";" + $installLocation 230 | Set-ItemProperty -Path 'Registry::HKEY_CURRENT_USER\Environment' -Name PATH -Value $newPath 231 | } 232 | 233 | info "Running clean up..." 234 | if (test-path $tmpDir) 235 | { 236 | verbose "Removing temporary directory" 237 | rm -r -fo $tmpDir 238 | } 239 | 240 | # Test if binary exists 241 | if (test-path $binaryFile) 242 | { 243 | verbose "Binary file exists" 244 | } 245 | else 246 | { 247 | error "Binary file does not exist" 248 | } 249 | 250 | Write-Host "" 251 | success "You can now run '$command' in your terminal!" 252 | info "You might have to restart your terminal session for the changes to take effect" 253 | 254 | . "../footer.txt" 255 | -------------------------------------------------------------------------------- /scripts/assets/linux/script.sh: -------------------------------------------------------------------------------- 1 | source ../header.txt 2 | 3 | # Import libraries 4 | source ./lib/colors.sh 5 | source ./lib/map.sh 6 | 7 | # Setup variables 8 | verbose="{{ .Verbose }}" 9 | if [ "$verbose" = "true" ]; then 10 | verbose=true 11 | else 12 | verbose=false 13 | fi 14 | 15 | # Pass variables from the go server into the script 16 | verbose "Setting up variables" 17 | owner="{{ .Owner }}" 18 | repo="{{ .Repo }}" 19 | 20 | verbose "Creating temporary directory" 21 | tmpDir="$(mktemp -d)" 22 | verbose "Temporary directory: $tmpDir" 23 | 24 | binaryLocation="$HOME/.local/bin" 25 | verbose "Binary location: $binaryLocation" 26 | mkdir -p "$binaryLocation" 27 | 28 | installLocation="$HOME/.local/bin/.instl/$repo" 29 | verbose "Install location: $installLocation" 30 | # Remove installLocation directory if it exists 31 | if [ -d "$installLocation" ]; then 32 | verbose "Removing existing install location" 33 | rm -rf "$installLocation" 34 | fi 35 | mkdir -p "$installLocation" 36 | 37 | # Print "INSTL" header 38 | source ../shared/intro.ps1 39 | 40 | # Installation 41 | curlOpts=("-sSL" "--retry" "5" "--retry-delay" "2" "--retry-max-time" "15") 42 | if [ -n "$GH_TOKEN" ]; then 43 | verbose "Using authentication with GH_TOKEN" 44 | curlOpts+=("--header" "Authorization: Bearer $GH_TOKEN") 45 | elif [ -n "$GITHUB_TOKEN" ]; then 46 | verbose "Using authentication with GITHUB_TOKEN" 47 | curlOpts+=("--header" "Authorization: Bearer $GITHUB_TOKEN") 48 | else 49 | verbose "No authentication" 50 | fi 51 | 52 | # GitHub public API 53 | latestReleaseURL="https://api.github.com/repos/$owner/$repo/releases/latest" 54 | verbose "Getting latest release from GitHub" 55 | getReleaseArgs=("${curlOpts[@]}" "$latestReleaseURL") 56 | verbose "Release curl args: ${getReleaseArgs[*]}" 57 | releaseJSON="$(curl "${getReleaseArgs[@]}")" 58 | 59 | tagName="$(echo "$releaseJSON" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')" 60 | info "Found latest release of $repo (version: $tagName)" 61 | 62 | # Get list of assets 63 | verbose "Parsing release json" 64 | assets=$(echo "$releaseJSON" | grep "browser_download_url" | sed -E 's/.*"([^"]+)".*/\1/') 65 | verbose "Found assets:" 66 | verbose "$assets" 67 | assetCount="$(echo "$assets" | wc -l | sed -E 's/^[[:space:]]*//')" 68 | info "Found $assetCount assets in '$tagName' - searching for one that fits your system..." 69 | 70 | # Get architecture of host 71 | arch="$(uname -m)" 72 | # Convert arch to lowercase 73 | arch="$(echo "$arch" | tr '[:upper:]' '[:lower:]')" 74 | verbose "Host architecture: $arch" 75 | 76 | # Set aliases for architectures 77 | amd64=("amd64" "x86_64" "x86-64" "x64") 78 | amd32=("386" "i386" "i686" "x86") 79 | arm=("arm" "armv7" "armv6" "armv8" "armv8l" "armv7l" "armv6l" "armv8l" "armv7l" "armv6l") 80 | 81 | currentArchAliases=() 82 | # Set the right aliases for current host system 83 | if [ "$arch" == "x86_64" ]; then 84 | currentArchAliases=("${amd64[@]}") 85 | elif [ "$arch" == "i386" ] || [ "$arch" == "i686" ]; then 86 | currentArchAliases=("${amd32[@]}") 87 | # Else if starts with "arm" 88 | elif [[ "$arch" =~ ^arm ]]; then 89 | currentArchAliases=("${arm[@]}") 90 | else 91 | error "Unsupported architecture: $arch" 92 | fi 93 | verbose "Current architecture aliases: ${currentArchAliases[*]}" 94 | 95 | # Get operating system of host 96 | os="$(uname -s)" 97 | # Convert os to lowercase 98 | os="$(echo "$os" | tr '[:upper:]' '[:lower:]')" 99 | verbose "Host operating system: $os" 100 | 101 | # Set aliases for operating systems 102 | linux=("linux") 103 | darwin=("darwin" "macos" "osx") 104 | 105 | currentOsAliases=() 106 | # If current os is linux, add linux aliases to the curentOsAliases array 107 | if [ "${os}" == "linux" ]; then 108 | currentOsAliases+=("${linux[@]}") 109 | elif [ "${os}" == "darwin" ]; then 110 | currentOsAliases+=("${darwin[@]}") 111 | fi 112 | verbose "Current operating system aliases: ${currentOsAliases[*]}" 113 | 114 | # Create map of assets and a score 115 | for asset in $assets; do 116 | score=0 117 | 118 | # Get file name from asset path 119 | fileName="$(echo "$asset" | awk -F'/' '{ print $NF; }')" 120 | # Set filename to lowercase 121 | fileName="$(echo "$fileName" | tr '[:upper:]' '[:lower:]')" 122 | 123 | # Set score to one, if the file name contains the current os 124 | for osAlias in "${currentOsAliases[@]}"; do 125 | if [[ "${fileName}" == *"$osAlias"* ]]; then 126 | score=10 127 | break 128 | fi 129 | done 130 | 131 | # Add two to the score for every alias that matches the current architecture 132 | for archAlias in "${currentArchAliases[@]}"; do 133 | if [[ "${fileName}" == *"$archAlias"* ]]; then 134 | verbose "Adding one to score for asset $fileName because it matches architecture $archAlias" 135 | score=$((score + 2)) 136 | fi 137 | done 138 | 139 | # Add one to the score if the file name contains .tar or .tar.gz or .tar.bz2 140 | if [[ "$fileName" == *".tar" ]] || [[ "$fileName" == *".tar.gz" ]] || [[ "$fileName" == *".tar.bz2" ]]; then 141 | verbose "Adding one to score for asset $fileName because it is a .tar or .tar.gz or .tar.bz2 file" 142 | score=$((score + 1)) 143 | fi 144 | 145 | # Add two to the score if the file name contains the repo name 146 | if [[ "$fileName" == *"$repo"* ]]; then 147 | verbose "Adding two to score for asset $fileName because it contains the repo name" 148 | score=$((score + 2)) 149 | fi 150 | 151 | # Add one to the score if the file name is exactly the repo name 152 | if [[ "$fileName" == "$repo" ]]; then 153 | verbose "Adding one to score for asset $fileName because it is exactly the repo name" 154 | score=$((score + 1)) 155 | fi 156 | 157 | # Initialize asset with score 158 | map_put assets "$fileName" "$score" 159 | done 160 | 161 | # Get map entry with highest score 162 | verbose "Finding asset with highest score" 163 | maxScore=0 164 | maxKey="" 165 | for asset in $(map_keys assets); do 166 | score="$(map_get assets "$asset")" 167 | if [ $score -gt $maxScore ]; then 168 | maxScore=$score 169 | maxKey=$asset 170 | fi 171 | verbose "Asset: $asset, score: $score" 172 | done 173 | 174 | assetName="$maxKey" 175 | 176 | # Check if asset name is still empty 177 | if [ -z "$assetName" ]; then 178 | error "Could not find any assets that fit your system" 179 | fi 180 | 181 | # Get asset URL from release assets 182 | assetURL="$(echo "$assets" | grep -i "$assetName")" 183 | 184 | info "Found asset with highest match score: $assetName" 185 | 186 | info "Downloading asset..." 187 | # Download asset 188 | downloadAssetArgs=$curlOpts 189 | downloadAssetArgs+=(-L "$assetURL" -o "$tmpDir/$assetName") 190 | verbose "${downloadAssetArgs[@]}" 191 | curl "${downloadAssetArgs[@]}" 192 | 193 | # Unpack asset if it is compressed 194 | if [[ "$assetName" == *".tar" ]]; then 195 | verbose "Unpacking .tar asset to $tmpDir" 196 | tar -xf "$tmpDir/$assetName" -C "$tmpDir" 197 | verbose "Removing packed asset ($tmpDir/$assetName)" 198 | rm "$tmpDir/$assetName" 199 | elif [[ "$assetName" == *".tar.gz" ]]; then 200 | verbose "Unpacking .tar.gz asset to $tmpDir" 201 | tar -xzf "$tmpDir/$assetName" -C "$tmpDir" 202 | verbose "Removing packed asset ($tmpDir/$assetName)" 203 | rm "$tmpDir/$assetName" 204 | elif [[ "$assetName" == *".gz" ]]; then 205 | verbose "Unpacking .gz asset to $tmpDir/$repo" 206 | gunzip -c "$tmpDir/$assetName" >"$tmpDir/$repo" 207 | verbose "Removing packed asset ($tmpDir/$assetName)" 208 | rm "$tmpDir/$assetName" 209 | verbose "Setting asset name to $repo, because it is a .gz file" 210 | assetName="$repo" 211 | verbose "Marking asset as executable" 212 | chmod +x "$tmpDir/$repo" 213 | elif [[ "$assetName" == *".tar.bz2" ]]; then 214 | verbose "Unpacking .tar.bz2 asset to $tmpDir" 215 | tar -xjf "$tmpDir/$assetName" -C "$tmpDir" 216 | verbose "Removing packed asset" 217 | rm "$tmpDir/$assetName" 218 | elif [[ "$assetName" == *".zip" ]]; then 219 | verbose "Unpacking .zip asset to $tmpDir/$repo" 220 | unzip "$tmpDir/$assetName" -d "$tmpDir" >/dev/null 2>&1 221 | verbose "Removing packed asset ($tmpDir/$assetName)" 222 | rm "$tmpDir/$assetName" 223 | else 224 | verbose "Asset is not a tar or zip file. Skipping unpacking." 225 | fi 226 | 227 | # If it was unpacked to a single directory, move the files to the root of the tmpDir 228 | # Also check that there are not other non directory files in the tmpDir 229 | verbose "Checking if asset was unpacked to a single directory" 230 | if [ "$(ls -d "$tmpDir"/* | wc -l)" -eq 1 ] && [ -d "$(ls -d "$tmpDir"/*)" ]; then 231 | verbose "Asset was unpacked to a single directory" 232 | verbose "Moving files to root of tmpDir" 233 | mv "$tmpDir"/*/* "$tmpDir" 234 | else 235 | verbose "Asset was not unpacked to a single directory" 236 | fi 237 | 238 | # Copy files to install location 239 | info "Installing '$repo'" 240 | verbose "Copying files to install location" 241 | # verbose print tmpDir files 242 | verbose "Files in $tmpDir:" 243 | verbose "$(ls "$tmpDir")" 244 | cp -r "$tmpDir"/* "$installLocation" 245 | 246 | # Find binary in install location to symlink to it later 247 | verbose "Finding binary in install location" 248 | binary="" 249 | for file in "$installLocation"/*; do 250 | # Check if the file is a binary file 251 | verbose "Checking if $file is a binary file" 252 | if [ -f "$file" ] && [ -x "$file" ]; then 253 | binary="$file" 254 | verbose "Found binary: $binary" 255 | break 256 | fi 257 | done 258 | 259 | # Get name of binary 260 | binaryName="$(basename "$binary")" 261 | verbose "Binary name: $binaryName" 262 | 263 | # Check if binary is empty 264 | if [ -z "$binary" ]; then 265 | error "Could not find binary in install location" 266 | fi 267 | 268 | # Remove previous symlink if it exists 269 | verbose "Removing previous symlink" 270 | rm "$binaryLocation/$repo" >/dev/null 2>&1 || true 271 | # Create symlink to binary 272 | verbose "Creating symlink '$binaryLocation/$binaryName' -> '$binary'" 273 | ln -s "$binary" "$binaryLocation/$binaryName" 274 | 275 | # Add binaryLocation to PATH, if it is not already in PATH 276 | if ! echo "$PATH" | grep -q "$binaryLocation"; then 277 | verbose "Adding $binaryLocation to PATH" 278 | # Array of common shell configuration files 279 | config_files=("$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.zshrc" "$HOME/.profile") 280 | for config in "${config_files[@]}"; do 281 | # Check if the file exists 282 | if [ -f "$config" ]; then 283 | # Check if binaryLocation is already in the file 284 | if ! grep -q -s "export PATH=$binaryLocation:\$PATH" "$config"; then 285 | verbose "Appending $binaryLocation to $config" 286 | echo "" >>"$config" 287 | echo "# Instl.sh installer binary location" >>"$config" 288 | echo "export PATH=$binaryLocation:\$PATH" >>"$config" 289 | else 290 | verbose "$binaryLocation is already in $config" 291 | fi 292 | else 293 | verbose "$config does not exist. Skipping append." 294 | fi 295 | done 296 | else 297 | verbose "$binaryLocation is already in PATH" 298 | fi 299 | 300 | info "Running clean up..."https://github.com/installer/instl 301 | verbose "Removing temporary directory" 302 | rm -rf "$tmpDir" 303 | 304 | # Test if binary exists 305 | if [ ! -f "$binary" ]; then 306 | error "Binary does not exist in installation location" 307 | else 308 | verbose "Binary exists in installation location" 309 | fi 310 | 311 | echo 312 | success "You can now run '$binaryName' in your terminal!" 313 | info "You might have to restart your terminal session for the changes to take effect" 314 | 315 | source ../footer.txt 316 | --------------------------------------------------------------------------------