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 |
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 |
18 |
Anonymized Session IDs: We collect anonymized session IDs for the purpose of page analytics. No cookies are set on your browser for tracking.
19 |
Cloudflare: We use Cloudflare as a reverse-proxy service. This means Cloudflare has visibility into your IP address. For more information, please consult Cloudflare's Privacy Policy.
20 |
Anonymized Request Logs: We maintain anonymized request logs to identify and fix bugs.
21 |
Installation Statistics: We track installations via instl for public statistics available at /stats.
22 |
GitHub Username and Repository: When you use instl, your GitHub username and repository name will be publicly displayed in the stats. If you wish to remove this data, please contact us via GitHub.
23 |
24 |
25 |
Use of Collected Information
26 |
27 |
We use the information collected for the following purposes:
28 |
29 |
30 |
To improve the quality and functionality of instl.
31 |
To gain insights into how users are engaging with our service.
32 |
To identify and rectify any technical issues or bugs.
33 |
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.
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.
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 |
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 |
32 |
33 |
Key Features
34 |
35 |
💻 Cross-Platform: Works on Windows, macOS and Linux out of the box
36 |
🧸 One-Click Installs: Install any GitHub project with just a single command
37 |
🪶 Web-Based: No need to install Instl - scripts are generated server-side
38 |
⚙️ Intelligent Configuration: Instl adapts to the project's structure
39 |
🕊️ On the fly: Installer scripts are created in real-time for each project
40 |
📊 Track Your Installs: Installation metrics for your projects at your fingertips
41 |
42 |
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 |
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 |
[](https://instl.sh/{{.Owner}}/{{.Repo}})