├── .github └── workflows │ └── release.yaml ├── .gitignore ├── .goreleaser.yaml ├── LICENSE ├── README.md ├── cmd └── aidoku │ ├── cmd │ ├── build.go │ ├── completions.go │ ├── init.go │ ├── logcat.go │ ├── resources │ │ └── schemas │ │ │ ├── filters.schema.json │ │ │ ├── settings.schema.json │ │ │ └── source.schema.json │ ├── root.go │ ├── serve.go │ ├── verify.go │ └── version.go │ └── main.go ├── go.mod ├── go.sum ├── internal ├── build │ ├── build.go │ └── web │ │ └── index.html.tmpl ├── common │ └── common.go └── templates │ ├── resources │ ├── common │ │ └── res │ │ │ ├── filters.json.tmpl │ │ │ ├── settings.json.tmpl │ │ │ └── source.json.tmpl │ ├── rust-template │ │ ├── Cargo.toml.tmpl │ │ ├── build.ps1.tmpl │ │ ├── build.sh.tmpl │ │ └── template │ │ │ ├── Cargo.toml.tmpl │ │ │ └── src │ │ │ ├── lib.rs.tmpl │ │ │ └── template.rs.tmpl │ └── rust │ │ ├── .cargo │ │ └── config.tmpl │ │ ├── Cargo.toml.tmpl │ │ ├── build.ps1.tmpl │ │ ├── build.sh.tmpl │ │ └── src │ │ └── lib.rs.tmpl │ ├── rust-template.go │ ├── rust.go │ └── templates.go └── scripts └── completions /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - v*.*.* 6 | 7 | jobs: 8 | goreleaser: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - 12 | name: Checkout 13 | uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | - 17 | name: Fetch all tags 18 | run: git fetch --force --tags 19 | - 20 | name: Set up Go 21 | uses: actions/setup-go@v3 22 | with: 23 | go-version: 1.18 24 | - 25 | name: Install build dependencies 26 | run: | 27 | go install github.com/GeertJohan/go.rice/rice@v1.0.2 28 | - 29 | name: Run GoReleaser 30 | uses: goreleaser/goreleaser-action@v2 31 | with: 32 | distribution: goreleaser 33 | version: 1.11.2 34 | args: release --rm-dist 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }} 37 | - 38 | name: Upload assets 39 | uses: actions/upload-artifact@v3 40 | with: 41 | name: disass_linux 42 | path: dist/* 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/Go 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=Go 4 | 5 | ### Go ### 6 | # If you prefer the allow list template instead of the deny list, see community template: 7 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 8 | # 9 | # Binaries for programs and plugins 10 | *.exe 11 | *.exe~ 12 | *.dll 13 | *.so 14 | *.dylib 15 | 16 | # Test binary, built with `go test -c` 17 | *.test 18 | 19 | # Output of the go coverage tool, specifically when used with LiteIDE 20 | *.out 21 | 22 | # Dependency directories (remove the comment below to include it) 23 | # vendor/ 24 | 25 | # Go workspace file 26 | go.work 27 | 28 | ### Go Patch ### 29 | /vendor/ 30 | /Godeps/ 31 | 32 | # End of https://www.toptal.com/developers/gitignore/api/Go 33 | 34 | # Aidoku package files 35 | *.aix 36 | 37 | # Default build folder 38 | /public/ 39 | dist/ 40 | completions/ 41 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | project_name: aidoku-cli 4 | 5 | release: 6 | prerelease: auto 7 | draft: false 8 | name_template: "aidoku-cli v{{.Version}}" 9 | 10 | before: 11 | hooks: 12 | - go mod tidy 13 | - sh ./scripts/completions 14 | builds: 15 | - main: ./cmd/aidoku 16 | binary: aidoku 17 | env: 18 | - CGO_ENABLED=0 19 | goos: 20 | - linux 21 | - windows 22 | - darwin 23 | mod_timestamp: "{{ .CommitTimestamp }}" 24 | flags: 25 | - -trimpath 26 | hooks: 27 | post: 28 | - rice append -i ./cmd/aidoku/cmd -i ./internal/templates -i ./internal/build --exec "{{ .Path }}" 29 | ldflags: | 30 | -s -w 31 | -X github.com/Aidoku/aidoku-cli/cmd/aidoku/cmd.version={{.Version}} 32 | -X github.com/Aidoku/aidoku-cli/cmd/aidoku/cmd.commit={{.FullCommit}} 33 | -X github.com/Aidoku/aidoku-cli/cmd/aidoku/cmd.date={{.Date}} 34 | -X github.com/Aidoku/aidoku-cli/cmd/aidoku/cmd.builtBy=goreleaser 35 | archives: 36 | - replacements: 37 | darwin: macos 38 | 386: i386 39 | amd64: x86_64 40 | wrap_in_directory: 'false' 41 | format: tar.gz 42 | format_overrides: 43 | - goos: windows 44 | format: zip 45 | checksum: 46 | name_template: 'checksums.txt' 47 | snapshot: 48 | name_template: "{{ incpatch .Version }}-next" 49 | brews: 50 | - tap: 51 | owner: beerpiss 52 | name: homebrew-tap 53 | folder: Formula 54 | homepage: https://github.com/Aidoku/aidoku-cli 55 | description: Aidoku development toolkit 56 | commit_msg_template: "{{.ProjectName}}: Update to version {{.Tag}}" 57 | license: 0BSD 58 | install: | 59 | bin.install "aidoku" 60 | (bash_completion/"aidoku").write `#{bin}/aidoku completion bash` 61 | (zsh_completion/"_aidoku").write `#{bin}/aidoku completion zsh` 62 | (fish_completion/"aidoku.fish").write `#{bin}/aidoku completion fish` 63 | test: | 64 | system "#{bin}/aidoku --version" 65 | 66 | changelog: 67 | sort: asc 68 | use: github 69 | groups: 70 | - title: "New features" 71 | regexp: "^.*feat[(\\w)]*:+.*$" 72 | order: 0 73 | - title: "Bug fixes" 74 | regexp: "^.*fix[(\\w)]*:+.*$" 75 | order: 10 76 | - title: Other work 77 | order: 999 78 | filters: 79 | exclude: 80 | - '^docs:' 81 | - '^test:' 82 | - '^chore:' 83 | - '^ci:' 84 | - Merge pull request 85 | - Merge remote-tracking branch 86 | - Merge branch 87 | - go mod tidy 88 | 89 | nfpms: 90 | - license: 0BSD 91 | maintainer: beerpsi 92 | homepage: https://github.com/Aidoku/aidoku-cli 93 | bindir: /usr/bin 94 | description: Aidoku development toolkit 95 | formats: 96 | - deb 97 | - rpm 98 | - apk 99 | contents: 100 | - src: completions/_bash 101 | dst: /usr/share/bash-completion/completions/aidoku 102 | - src: completions/_zsh 103 | dst: /usr/share/zsh/site-functions/_aidoku 104 | - src: completions/_fish 105 | dst: /usr/share/fish/vendor_completions.d/aidoku.fish 106 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD Zero Clause License 2 | 3 | Copyright (c) 2022 beerpsi 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 10 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 12 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 13 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 14 | PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aidoku-cli 2 | Aidoku development tools in a single program 3 | 4 | # Installation 5 | ```sh 6 | # macOS/Linux 7 | brew install beerpiss/tap/aidoku 8 | 9 | # Windows 10 | scoop bucket add beerpiss https://github.com/beerpiss/scoop-bucket 11 | scoop install beerpiss/aidoku-cli 12 | 13 | # if you have golang installed 14 | go install github.com/Aidoku/aidoku-cli/...@latest 15 | ``` 16 | or download them from [Releases](https://github.com/beerpiss/aidoku-cli/releases) 17 | 18 | # Usage 19 | ```pwsh 20 | Aidoku development toolkit 21 | 22 | Usage: 23 | aidoku [command] 24 | 25 | Available Commands: 26 | build Build a source list from packages 27 | completion Generate completion script 28 | help Help about any command 29 | init Create initial code for an Aidoku source 30 | logcat Log streaming 31 | serve Build a source list and serve it on the local network 32 | verify Test Aidoku packages if they're ready for publishing 33 | version Print version 34 | 35 | Flags: 36 | -h, --help help for aidoku 37 | -v, --verbose verbose output 38 | --version version for aidoku 39 | 40 | Use "aidoku [command] --help" for more information about a command. 41 | ``` 42 | 43 | # Commands 44 | ## `aidoku verify ` 45 | ```pwsh 46 | Test Aidoku packages if they're ready for publishing 47 | 48 | Usage: 49 | aidoku verify [flags] 50 | 51 | Flags: 52 | -h, --help help for verify 53 | 54 | Global Flags: 55 | -v, --verbose verbose output 56 | ``` 57 | 58 | ## `aidoku init [rust-template|rust|as|c] [DIR]` 59 | ```sh 60 | Create initial code for an Aidoku source 61 | 62 | Usage: 63 | aidoku init [rust-template|rust|as|c] [DIR] [flags] 64 | 65 | Flags: 66 | -h, --help help for init 67 | -p, --homepage string Source homepage 68 | -l, --language string Source language 69 | -n, --name string Source name 70 | --nsfw int Source NSFW level (default -1) 71 | --version version for init 72 | 73 | Global Flags: 74 | -v, --verbose verbose output 75 | ``` 76 | 77 | ## `aidoku build ` 78 | ```sh 79 | Build a source list from packages 80 | 81 | Usage: 82 | aidoku build [flags] 83 | 84 | Flags: 85 | -h, --help help for build 86 | -o, --output string Output folder (default "public") 87 | -w, --web Generate a landing page for the source list 88 | --web-title string Title of the landing page (default "An Aidoku source list") 89 | 90 | Global Flags: 91 | --force-color always output with color 92 | -v, --verbose verbose output 93 | ``` 94 | 95 | ## `aidoku serve ` 96 | ```sh 97 | Build a source list and serve it on the local network 98 | 99 | Usage: 100 | aidoku serve [flags] 101 | 102 | Flags: 103 | -h, --help help for serve 104 | -o, --output string The source list folder (default "public") 105 | -p, --port string The port to broadcast the source list on (default "8080") 106 | 107 | Global Flags: 108 | -v, --verbose verbose output 109 | ``` 110 | 111 | ## `aidoku logcat` 112 | ```sh 113 | Log streaming 114 | 115 | Usage: 116 | aidoku logcat [flags] 117 | 118 | Flags: 119 | -h, --help help for logcat 120 | -p, --port string The port to listen to logs on (default "9000") 121 | 122 | Global Flags: 123 | -v, --verbose verbose output 124 | ``` 125 | 126 | ## `aidoku completion ` 127 | ``` 128 | Generate completion script 129 | 130 | Usage: 131 | aidoku completion [bash|zsh|fish|powershell] 132 | 133 | Flags: 134 | -h, --help help for completion 135 | --version version for completion 136 | 137 | Global Flags: 138 | -v, --verbose verbose output 139 | ``` 140 | -------------------------------------------------------------------------------- /cmd/aidoku/cmd/build.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/Aidoku/aidoku-cli/internal/build" 5 | "github.com/fatih/color" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var buildCmd = &cobra.Command{ 10 | Use: "build ", 11 | Short: "Build a source list from packages", 12 | Version: rootCmd.Version, 13 | Args: cobra.MinimumNArgs(1), 14 | SilenceUsage: true, 15 | SilenceErrors: true, 16 | RunE: func(cmd *cobra.Command, args []string) error { 17 | if ForceColor { 18 | color.NoColor = false 19 | } 20 | flags := cmd.Flags() 21 | 22 | output, _ := flags.GetString("output") 23 | 24 | web, _ := flags.GetBool("web") 25 | webTitle, _ := flags.GetString("web-title") 26 | webDescription, _ := flags.GetString("web-description") 27 | webIcon, _ := flags.GetString("web-icon") 28 | 29 | webArgs := build.WebTemplateArguments{ 30 | Title: webTitle, 31 | Description: webDescription, 32 | Icon: webIcon, 33 | } 34 | 35 | return build.BuildWrapper(args, output, web, webArgs) 36 | }, 37 | } 38 | 39 | func init() { 40 | rootCmd.AddCommand(buildCmd) 41 | buildCmd.Flags().StringP("output", "o", "public", "Output folder") 42 | 43 | buildCmd.Flags().BoolP("web", "w", false, "Generate a landing page for the source list") 44 | buildCmd.Flags().String("web-title", "An Aidoku source list", "Title of the landing page") 45 | buildCmd.Flags().String("web-description", "A source list for use with Aidoku.", "Description of the landing page") 46 | buildCmd.Flags().String("web-icon", "https://aidoku.app/images/favicon-32x32.png", "Icon of the landing page") 47 | 48 | buildCmd.MarkZshCompPositionalArgumentFile(1, "*.aix") 49 | buildCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 50 | return []string{"aix"}, cobra.ShellCompDirectiveFilterFileExt 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /cmd/aidoku/cmd/completions.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var completionCmd = &cobra.Command{ 10 | Use: "completion [bash|zsh|fish|powershell]", 11 | Short: "Generate completion script", 12 | Version: rootCmd.Version, 13 | DisableFlagsInUseLine: true, 14 | ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, 15 | Args: cobra.ExactValidArgs(1), 16 | Run: func(cmd *cobra.Command, args []string) { 17 | switch args[0] { 18 | case "bash": 19 | cmd.Root().GenBashCompletion(os.Stdout) 20 | case "zsh": 21 | cmd.Root().GenZshCompletion(os.Stdout) 22 | case "fish": 23 | cmd.Root().GenFishCompletion(os.Stdout, true) 24 | case "powershell": 25 | cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) 26 | } 27 | }, 28 | } 29 | 30 | func init() { 31 | rootCmd.AddCommand(completionCmd) 32 | } 33 | -------------------------------------------------------------------------------- /cmd/aidoku/cmd/init.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "net/url" 6 | "os" 7 | "strings" 8 | 9 | "golang.org/x/exp/slices" 10 | "golang.org/x/text/language" 11 | 12 | "github.com/Aidoku/aidoku-cli/internal/templates" 13 | "github.com/AlecAivazis/survey/v2" 14 | "github.com/AlecAivazis/survey/v2/terminal" 15 | "github.com/fatih/color" 16 | "github.com/spf13/cobra" 17 | ) 18 | 19 | func checkPrompt(err error) error { 20 | if err != nil { 21 | if err == terminal.InterruptErr { 22 | color.Red("interrupted") 23 | os.Exit(1) 24 | } 25 | } 26 | return nil 27 | } 28 | 29 | func languageValidator(lang string) bool { 30 | return slices.Contains([]string{"rust-template", "rust", "as", "c"}, lang) 31 | } 32 | 33 | func sourceLanguageValidator(response interface{}) error { 34 | if response.(string) != "multi" { 35 | _, err := language.Parse(response.(string)) 36 | return err 37 | } else { 38 | return nil 39 | } 40 | } 41 | 42 | var initCommand = &cobra.Command{ 43 | Use: "init [rust-template|rust|as|c] [DIR]", 44 | Short: "Create initial code for an Aidoku source", 45 | Version: rootCmd.Version, 46 | SilenceUsage: true, 47 | SilenceErrors: true, 48 | RunE: func(cmd *cobra.Command, args []string) error { 49 | lang, _ := cmd.Flags().GetString("language") 50 | name, _ := cmd.Flags().GetString("name") 51 | homepage, _ := cmd.Flags().GetString("homepage") 52 | nsfw, _ := cmd.Flags().GetInt("nsfw") 53 | if len(args) < 1 || !languageValidator(args[0]) { 54 | prompt := &survey.Select{ 55 | Message: "Template", 56 | Options: []string{"rust-template", "rust", "as", "c"}, 57 | } 58 | var selection string 59 | checkPrompt(survey.AskOne(prompt, &selection, survey.WithValidator(survey.Required))) 60 | args = append(args, selection) 61 | } 62 | if len(args) < 2 { 63 | prompt := &survey.Input{ 64 | Message: "Directory to generate template", 65 | Default: ".", 66 | } 67 | var selection string 68 | checkPrompt(survey.AskOne(prompt, &selection)) 69 | args = append(args, selection) 70 | } 71 | if name == "" { 72 | prompt := &survey.Input{ 73 | Message: "Source name", 74 | } 75 | var result string 76 | checkPrompt(survey.AskOne(prompt, &result, survey.WithValidator(survey.Required))) 77 | name = result 78 | } 79 | if lang == "" { 80 | prompt := &survey.Input{ 81 | Message: "Source language", 82 | } 83 | var result string 84 | checkPrompt(survey.AskOne(prompt, &result, survey.WithValidator(sourceLanguageValidator))) 85 | lang = result 86 | } 87 | if homepage == "" { 88 | prompt := &survey.Input{ 89 | Message: "Source homepage", 90 | } 91 | var result string 92 | checkPrompt(survey.AskOne(prompt, &result, survey.WithValidator(func(response interface{}) error { 93 | _, err := url.ParseRequestURI(response.(string)) 94 | return err 95 | }))) 96 | homepage = result 97 | } 98 | if nsfw == -1 { 99 | prompt := &survey.Select{ 100 | Message: "Source NSFW level", 101 | Options: []string{"None", "Moderate amounts of sex", "Insane Amounts of Sex"}, 102 | } 103 | var selection int 104 | checkPrompt(survey.AskOne(prompt, &selection, survey.WithValidator(survey.Required))) 105 | nsfw = selection 106 | } 107 | 108 | source := templates.Source{ 109 | Name: name, 110 | Homepage: homepage, 111 | Language: lang, 112 | Nsfw: nsfw, 113 | } 114 | 115 | var err error 116 | switch args[0] { 117 | case "rust": 118 | { 119 | // Determine if it's a child of a template by checking for a parent package at $output 120 | if file, err := os.Open(args[1] + "/../../template/Cargo.toml"); err == nil { 121 | defer file.Close() 122 | scanner := bufio.NewScanner(file) 123 | for scanner.Scan() { 124 | line := scanner.Text() 125 | if strings.Contains(line, "name = \"") { 126 | source.TemplateName = strings.TrimSuffix(strings.TrimPrefix(line, "name = \""), "\"") 127 | break 128 | } 129 | } 130 | } 131 | err = templates.RustGenerator(args[1], source) 132 | } 133 | case "rust-template": 134 | err = templates.RustTemplateGenerator(args[1], source) 135 | } 136 | if err != nil { 137 | color.Red("error: could not generate initial code") 138 | } 139 | return err 140 | }, 141 | } 142 | 143 | func init() { 144 | rootCmd.AddCommand(initCommand) 145 | initCommand.Flags().StringP("name", "n", "", "Source name") 146 | initCommand.Flags().StringP("language", "l", "", "Source language") 147 | initCommand.Flags().StringP("homepage", "p", "", "Source homepage") 148 | initCommand.Flags().Int("nsfw", -1, "Source NSFW level") 149 | } 150 | -------------------------------------------------------------------------------- /cmd/aidoku/cmd/logcat.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "strings" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/Aidoku/aidoku-cli/internal/common" 14 | "github.com/fatih/color" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | func Logcat(w http.ResponseWriter, req *http.Request) { 19 | if req.Method != "POST" { 20 | fmt.Fprintf(w, "Method not supported\n") 21 | } else { 22 | buf := new(strings.Builder) 23 | io.Copy(buf, req.Body) 24 | 25 | items := strings.Split(buf.String(), "] [") 26 | t, err := time.Parse("01/02 03:04:05.999", strings.TrimLeft(items[0], "[")) 27 | if err == nil { 28 | t = time.Date(time.Now().Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location()) 29 | items[0] = fmt.Sprintf("[%s", t.Format("2006-01-02T15:04:05.999999999")) 30 | } 31 | log := strings.Join(items, "] [") 32 | 33 | if strings.Contains(log, "[ERROR]") { 34 | color.Red(log) 35 | } else if strings.Contains(log, "[WARN]") { 36 | color.Yellow(log) 37 | } else if strings.Contains(log, "[DEBUG]") { 38 | color.HiBlack(log) 39 | } else { 40 | fmt.Println(log) 41 | } 42 | } 43 | } 44 | 45 | var logcatCmd = &cobra.Command{ 46 | Use: "logcat", 47 | Short: "Log streaming", 48 | Version: rootCmd.Version, 49 | SilenceUsage: true, 50 | SilenceErrors: true, 51 | RunE: func(cmd *cobra.Command, args []string) error { 52 | c := make(chan os.Signal) 53 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 54 | go func() { 55 | <-c 56 | os.Exit(0) 57 | }() 58 | 59 | if ForceColor { 60 | color.NoColor = false 61 | } 62 | 63 | address, _ := cmd.Flags().GetString("address") 64 | port, _ := cmd.Flags().GetString("port") 65 | http.HandleFunc("/", Logcat) 66 | 67 | fmt.Println("Listening on these addresses:") 68 | if address == "0.0.0.0" { 69 | common.PrintAddresses(port) 70 | } else { 71 | color.Green(" http://%s:%s", address, port) 72 | } 73 | 74 | return http.ListenAndServe(address+":"+port, nil) 75 | }, 76 | } 77 | 78 | func init() { 79 | rootCmd.AddCommand(logcatCmd) 80 | logcatCmd.Flags().StringP("address", "a", "0.0.0.0", "Address to listen to logs on") 81 | logcatCmd.Flags().StringP("port", "p", "9000", "The port to listen to logs on") 82 | } 83 | -------------------------------------------------------------------------------- /cmd/aidoku/cmd/resources/schemas/filters.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "title": "Aidoku filters specification", 4 | "definitions": { 5 | "arrayOfStrings": { 6 | "type": "array", 7 | "items": { 8 | "type": "string" 9 | } 10 | }, 11 | "titleOrAuthorFilter": { 12 | "type": "object", 13 | "properties": { 14 | "type": { 15 | "type": "string", 16 | "enum": [ 17 | "title", 18 | "author" 19 | ] 20 | } 21 | } 22 | }, 23 | "selectFilter": { 24 | "type": "object", 25 | "properties": { 26 | "type": { 27 | "type": "string", 28 | "enum": [ 29 | "select" 30 | ] 31 | }, 32 | "name": { 33 | "type": "string" 34 | }, 35 | "options": { 36 | "$ref": "#/definitions/arrayOfStrings" 37 | }, 38 | "default": { 39 | "type": "integer" 40 | } 41 | }, 42 | "required": [ 43 | "type", 44 | "name", 45 | "options" 46 | ] 47 | }, 48 | "checkOrGenreFilter": { 49 | "type": "object", 50 | "properties": { 51 | "type": { 52 | "type": "string", 53 | "enum": [ 54 | "genre", 55 | "check" 56 | ] 57 | }, 58 | "name": { 59 | "type": "string" 60 | }, 61 | "canExclude": { 62 | "type": "boolean" 63 | }, 64 | "id": { 65 | "type": "string" 66 | }, 67 | "default": { 68 | "type": "boolean" 69 | } 70 | }, 71 | "required": [ 72 | "type", 73 | "name" 74 | ] 75 | }, 76 | "sortFilter": { 77 | "type": "object", 78 | "properties": { 79 | "type": { 80 | "type": "string", 81 | "enum": [ 82 | "sort" 83 | ] 84 | }, 85 | "name": { 86 | "type": "string" 87 | }, 88 | "canAscend": { 89 | "type": "boolean" 90 | }, 91 | "options": { 92 | "$ref": "#/definitions/arrayOfStrings" 93 | }, 94 | "default": { 95 | "type": "object", 96 | "properties": { 97 | "index": { 98 | "type": "integer" 99 | }, 100 | "ascending": { 101 | "type": "boolean" 102 | } 103 | } 104 | } 105 | }, 106 | "required": [ 107 | "type", 108 | "name", 109 | "options" 110 | ] 111 | }, 112 | "groupFilter": { 113 | "type": "object", 114 | "properties": { 115 | "type": { 116 | "type": "string", 117 | "enum": [ 118 | "group" 119 | ] 120 | }, 121 | "name": { 122 | "type": "string" 123 | }, 124 | "filters": { 125 | "type": "array", 126 | "items": { 127 | "$ref": "#/definitions/filter" 128 | } 129 | } 130 | }, 131 | "default": [ 132 | "type", 133 | "name", 134 | "filters" 135 | ] 136 | }, 137 | "filter": { 138 | "anyOf": [ 139 | { 140 | "$ref": "#/definitions/checkOrGenreFilter" 141 | }, 142 | { 143 | "$ref": "#/definitions/titleOrAuthorFilter" 144 | }, 145 | { 146 | "$ref": "#/definitions/selectFilter" 147 | }, 148 | { 149 | "$ref": "#/definitions/groupFilter" 150 | }, 151 | { 152 | "$ref": "#/definitions/sortFilter" 153 | } 154 | ] 155 | } 156 | }, 157 | "type": "array", 158 | "items": { 159 | "$ref": "#/definitions/filter" 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /cmd/aidoku/cmd/resources/schemas/settings.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "title": "Aidoku settings specification", 4 | "definitions": { 5 | "any": { 6 | "anyOf": [ 7 | { 8 | "type": "array", 9 | "items": { 10 | "$ref": "#/definitions/any" 11 | } 12 | }, 13 | { 14 | "type": "boolean" 15 | }, 16 | { 17 | "type": "integer" 18 | }, 19 | { 20 | "type": "null" 21 | }, 22 | { 23 | "type": "object", 24 | "additionalProperties": { 25 | "$ref": "#/definitions/any" 26 | } 27 | }, 28 | { 29 | "type": "string" 30 | } 31 | ] 32 | }, 33 | "destructableSettings": { 34 | "type": "object", 35 | "properties": { 36 | "destructive": { 37 | "description": "Marks an action as destructive.", 38 | "type": "boolean" 39 | } 40 | } 41 | }, 42 | "authToOpenSettings": { 43 | "type": "object", 44 | "properties": { 45 | "authToOpen": { 46 | "description": "Requires Face ID/Touch ID authentication to open an options page.", 47 | "type": "boolean" 48 | } 49 | } 50 | }, 51 | "requirableSettings": { 52 | "type": "object", 53 | "properties": { 54 | "requires": { 55 | "description": "Require that another setting be enabled before this one can be modified.", 56 | "type": "string" 57 | }, 58 | "requiresFalse": { 59 | "description": "Require that another setting be disabled before this one can be modified.", 60 | "type": "string" 61 | } 62 | } 63 | }, 64 | "titleableSettings": { 65 | "type": "object", 66 | "properties": { 67 | "title": { 68 | "description": "The title of the setting.", 69 | "type": "string" 70 | } 71 | }, 72 | "required": [ 73 | "title" 74 | ] 75 | }, 76 | "selectableSettings": { 77 | "type": "object", 78 | "allOf": [ 79 | { 80 | "$ref": "#/definitions/settingItemBase" 81 | }, 82 | { 83 | "type": "object", 84 | "properties": { 85 | "titles": { 86 | "description": "A list of display names for the options.", 87 | "type": "array", 88 | "items": { 89 | "type": "string" 90 | } 91 | }, 92 | "values": { 93 | "description": "The actual values for the options.", 94 | "type": "array", 95 | "items": { 96 | "type": "string" 97 | } 98 | } 99 | } 100 | } 101 | ] 102 | }, 103 | "settingItemBase": { 104 | "type": "object", 105 | "allOf": [ 106 | { 107 | "$ref": "#/definitions/requirableSettings" 108 | }, 109 | { 110 | "type": "object", 111 | "properties": { 112 | "key": { 113 | "description": "The defaults key to access the setting with.", 114 | "type": "string" 115 | }, 116 | "notification": { 117 | "description": "A notification to be posted whenever the value for this setting changes.", 118 | "type": "string" 119 | }, 120 | "default": { 121 | "description": "The default value for this setting.", 122 | "$ref": "#/definitions/any" 123 | } 124 | } 125 | } 126 | ] 127 | }, 128 | "switchSettings": { 129 | "type": "object", 130 | "allOf": [ 131 | { 132 | "$ref": "#/definitions/settingItemBase" 133 | }, 134 | { 135 | "$ref": "#/definitions/titleableSettings" 136 | }, 137 | { 138 | "properties": { 139 | "type": { 140 | "type": "string", 141 | "enum": [ 142 | "switch" 143 | ] 144 | }, 145 | "subtitle": { 146 | "description": "Setting subtitle, displayed as smaller gray text under the title.", 147 | "type": "string" 148 | }, 149 | "authToEnable": { 150 | "description": "Requires Face ID/Touch ID authentication to enable a switch.", 151 | "type": "boolean" 152 | }, 153 | "authToDisable": { 154 | "description": "Requires Face ID/Touch ID authentication to disable a switch.", 155 | "type": "boolean" 156 | }, 157 | "default": { 158 | "description": "The default value for this switch.", 159 | "type": "boolean" 160 | } 161 | }, 162 | "required": [ 163 | "type", 164 | "key" 165 | ] 166 | } 167 | ] 168 | }, 169 | "stepperSettings": { 170 | "type": "object", 171 | "allOf": [ 172 | { 173 | "$ref": "#/definitions/settingItemBase" 174 | }, 175 | { 176 | "$ref": "#/definitions/titleableSettings" 177 | }, 178 | { 179 | "type": "object", 180 | "properties": { 181 | "type": { 182 | "type": "string", 183 | "enum": [ 184 | "stepper" 185 | ] 186 | }, 187 | "minimumValue": { 188 | "description": "Minimum value for the stepper.", 189 | "type": "number" 190 | }, 191 | "maximumValue": { 192 | "description": "Maximum value for the stepper.", 193 | "type": "number" 194 | }, 195 | "default": { 196 | "description": "The default value for this setting.", 197 | "type": "number" 198 | } 199 | }, 200 | "required": [ 201 | "type", 202 | "key" 203 | ] 204 | } 205 | ] 206 | }, 207 | "textSettings": { 208 | "type": "object", 209 | "allOf": [ 210 | { 211 | "$ref": "#/definitions/settingItemBase" 212 | }, 213 | { 214 | "type": "object", 215 | "properties": { 216 | "type": { 217 | "type": "string", 218 | "enum": [ 219 | "text" 220 | ] 221 | }, 222 | "placeholder": { 223 | "description": "The placeholder for the text field.", 224 | "type": "string" 225 | }, 226 | "autocapitalizationType": { 227 | "description": "The auto-capitalization type for the text field. Refer to [UITextAutocapitalizationType](https://developer.apple.com/documentation/uikit/uitextautocapitalizationtype) for possible values.", 228 | "type": "integer", 229 | "minimum": 0, 230 | "maximum": 3 231 | }, 232 | "autocorrectionType": { 233 | "description": "The auto-correction behavior of the text field. Refer to [UITextAutocorrectionType](https://developer.apple.com/documentation/uikit/uitextautocorrectiontype) for possible values.", 234 | "type": "integer", 235 | "minimum": 0, 236 | "maximum": 2 237 | }, 238 | "spellCheckingType": { 239 | "description": "The spell-checking behavior of the text field. Refer to [UITextSpellCheckingType](https://developer.apple.com/documentation/uikit/uitextspellcheckingtype) for possible values.", 240 | "type": "integer", 241 | "minimum": 0, 242 | "maximum": 2 243 | }, 244 | "keyboardType": { 245 | "description": "Specify the kind of keyboard to display for the text field. Refer to [UIKeyboardType](https://developer.apple.com/documentation/uikit/uikeyboardtype) for possible values.", 246 | "type": "integer", 247 | "minimum": 0, 248 | "maximum": 12 249 | }, 250 | "returnKeyType": { 251 | "description": "Specify the text string that displays in the Return key. Refer to [UIReturnKeyType](https://developer.apple.com/documentation/uikit/uireturnkeytype) for possible values.", 252 | "type": "integer", 253 | "minimum": 0, 254 | "maximum": 12 255 | }, 256 | "default": { 257 | "description": "The default value for this setting.", 258 | "type": "string" 259 | } 260 | }, 261 | "required": [ 262 | "type", 263 | "key" 264 | ] 265 | } 266 | ] 267 | }, 268 | "buttonSettings": { 269 | "type": "object", 270 | "description": "A button that can be used to trigger an action. For this button to be useful, the setting should also have a `notification` key.", 271 | "allOf": [ 272 | { 273 | "$ref": "#/definitions/settingItemBase" 274 | }, 275 | { 276 | "$ref": "#/definitions/destructableSettings" 277 | }, 278 | { 279 | "$ref": "#/definitions/titleableSettings" 280 | }, 281 | { 282 | "type": "object", 283 | "properties": { 284 | "type": { 285 | "type": "string", 286 | "enum": [ 287 | "button" 288 | ] 289 | }, 290 | "action": { 291 | "description": "Notification that will be emitted when the button is pressed.", 292 | "type": "string" 293 | } 294 | }, 295 | "required": [ 296 | "type" 297 | ] 298 | } 299 | ] 300 | }, 301 | "linkSettings": { 302 | "type": "object", 303 | "description": "The link should be stored in the `key` property.", 304 | "allOf": [ 305 | { 306 | "$ref": "#/definitions/settingItemBase" 307 | }, 308 | { 309 | "$ref": "#/definitions/destructableSettings" 310 | }, 311 | { 312 | "$ref": "#/definitions/titleableSettings" 313 | }, 314 | { 315 | "type": "object", 316 | "properties": { 317 | "type": { 318 | "type": "string", 319 | "enum": [ 320 | "link" 321 | ] 322 | }, 323 | "url": { 324 | "description": "The URL to open.", 325 | "type": "string" 326 | }, 327 | "external": { 328 | "description": "Marks a link as external (opens in Safari intead of in-app browser).", 329 | "type": "boolean" 330 | } 331 | }, 332 | "required": [ 333 | "type", 334 | "url" 335 | ] 336 | } 337 | ] 338 | }, 339 | "segmentSettings": { 340 | "type": "object", 341 | "allOf": [ 342 | { 343 | "$ref": "#/definitions/selectableSettings" 344 | }, 345 | { 346 | "$ref": "#/definitions/titleableSettings" 347 | }, 348 | { 349 | "type": "object", 350 | "properties": { 351 | "type": { 352 | "type": "string", 353 | "enum": [ 354 | "segment" 355 | ] 356 | }, 357 | "default": { 358 | "description": "The default value for this setting.", 359 | "type": "string" 360 | } 361 | }, 362 | "required": [ 363 | "type", 364 | "key" 365 | ] 366 | } 367 | ] 368 | }, 369 | "selectSettings": { 370 | "type": "object", 371 | "allOf": [ 372 | { 373 | "$ref": "#/definitions/selectableSettings" 374 | }, 375 | { 376 | "$ref": "#/definitions/authToOpenSettings" 377 | }, 378 | { 379 | "$ref": "#/definitions/titleableSettings" 380 | }, 381 | { 382 | "type": "object", 383 | "properties": { 384 | "type": { 385 | "type": "string", 386 | "enum": [ 387 | "select" 388 | ] 389 | }, 390 | "default": { 391 | "description": "The default value for this setting.", 392 | "type": "string" 393 | } 394 | }, 395 | "required": [ 396 | "type", 397 | "key" 398 | ] 399 | } 400 | ] 401 | }, 402 | "multiSelectSettings": { 403 | "type": "object", 404 | "allOf": [ 405 | { 406 | "$ref": "#/definitions/selectableSettings" 407 | }, 408 | { 409 | "$ref": "#/definitions/authToOpenSettings" 410 | }, 411 | { 412 | "$ref": "#/definitions/titleableSettings" 413 | }, 414 | { 415 | "type": "object", 416 | "properties": { 417 | "type": { 418 | "type": "string", 419 | "enum": [ 420 | "multi-select", 421 | "multi-single-select" 422 | ] 423 | }, 424 | "default": { 425 | "description": "The default value for this setting.", 426 | "type": "array", 427 | "items": { 428 | "type": "string" 429 | } 430 | } 431 | }, 432 | "required": [ 433 | "type", 434 | "key" 435 | ] 436 | } 437 | ] 438 | }, 439 | "ungroupedSettings": { 440 | "oneOf": [ 441 | { 442 | "$ref": "#/definitions/switchSettings" 443 | }, 444 | { 445 | "$ref": "#/definitions/stepperSettings" 446 | }, 447 | { 448 | "$ref": "#/definitions/textSettings" 449 | }, 450 | { 451 | "$ref": "#/definitions/buttonSettings" 452 | }, 453 | { 454 | "$ref": "#/definitions/linkSettings" 455 | }, 456 | { 457 | "$ref": "#/definitions/segmentSettings" 458 | }, 459 | { 460 | "$ref": "#/definitions/selectSettings" 461 | }, 462 | { 463 | "$ref": "#/definitions/multiSelectSettings" 464 | } 465 | ] 466 | }, 467 | "pageSettings": { 468 | "type": "object", 469 | "allOf": [ 470 | { 471 | "$ref": "#/definitions/requirableSettings" 472 | }, 473 | { 474 | "$ref": "#/definitions/titleableSettings" 475 | }, 476 | { 477 | "type": "object", 478 | "properties": { 479 | "type": { 480 | "type": "string", 481 | "enum": [ 482 | "page" 483 | ] 484 | }, 485 | "items": { 486 | "type": "array", 487 | "items": { 488 | "anyOf": [ 489 | { 490 | "$ref": "#/definitions/ungroupedSettings" 491 | }, 492 | { 493 | "$ref": "#/definitions/groupSettings" 494 | } 495 | ] 496 | } 497 | } 498 | }, 499 | "required": [ 500 | "type", 501 | "items" 502 | ] 503 | } 504 | ] 505 | }, 506 | "groupSettings": { 507 | "type": "object", 508 | "allOf": [ 509 | { 510 | "properties": { 511 | "type": { 512 | "type": "string", 513 | "enum": [ 514 | "group" 515 | ] 516 | }, 517 | "title": { 518 | "description": "Header text of the group.", 519 | "type": "string" 520 | }, 521 | "footer": { 522 | "description": "Footer text of the group.", 523 | "type": "string" 524 | }, 525 | "items": { 526 | "type": "array", 527 | "items": { 528 | "$ref": "#/definitions/ungroupedSettings" 529 | } 530 | } 531 | }, 532 | "required": [ 533 | "type", 534 | "items" 535 | ] 536 | } 537 | ] 538 | }, 539 | "settings": { 540 | "$ref": "#/definitions/groupSettings" 541 | } 542 | }, 543 | "type": "array", 544 | "items": { 545 | "$ref": "#/definitions/settings" 546 | } 547 | } 548 | -------------------------------------------------------------------------------- /cmd/aidoku/cmd/resources/schemas/source.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "title": "Aidoku source specification", 4 | "definitions": { 5 | "language": { 6 | "type": "object", 7 | "properties": { 8 | "code": { 9 | "type": "string" 10 | }, 11 | "value": { 12 | "type": "string" 13 | }, 14 | "default": { 15 | "type": "boolean" 16 | } 17 | }, 18 | "required": [ 19 | "code" 20 | ] 21 | }, 22 | "sourceInfo": { 23 | "type": "object", 24 | "properties": { 25 | "id": { 26 | "type": "string" 27 | }, 28 | "lang": { 29 | "type": "string" 30 | }, 31 | "name": { 32 | "type": "string" 33 | }, 34 | "version": { 35 | "type": "integer", 36 | "minimum": 1 37 | }, 38 | "url": { 39 | "type": "string" 40 | }, 41 | "urls": { 42 | "type": "array", 43 | "items": { 44 | "type": "string" 45 | } 46 | }, 47 | "nsfw": { 48 | "type": "integer", 49 | "minimum": 0, 50 | "maximum": 2 51 | }, 52 | "minAppVersion": { 53 | "type": "string", 54 | "pattern": "\\d\\.\\d" 55 | }, 56 | "maxAppVersion": { 57 | "type": "string", 58 | "pattern": "\\d\\.\\d" 59 | } 60 | }, 61 | "required": [ 62 | "id", 63 | "lang", 64 | "name", 65 | "version" 66 | ] 67 | }, 68 | "listing": { 69 | "type": "object", 70 | "properties": { 71 | "name": { 72 | "type": "string" 73 | }, 74 | "flags": { 75 | "description": "Currently unused", 76 | "type": "integer" 77 | } 78 | }, 79 | "required": [ 80 | "name" 81 | ] 82 | } 83 | }, 84 | "type": "object", 85 | "properties": { 86 | "info": { 87 | "$ref": "#/definitions/sourceInfo" 88 | }, 89 | "languages": { 90 | "type": "array", 91 | "items": { 92 | "$ref": "#/definitions/language" 93 | } 94 | }, 95 | "listings": { 96 | "type": "array", 97 | "items": { 98 | "$ref": "#/definitions/listing" 99 | } 100 | }, 101 | "languageSelectType": { 102 | "type": "string", 103 | "enum": [ 104 | "single", 105 | "multi" 106 | ], 107 | "description": "Accepts any value, but only \"single\" has meaning" 108 | } 109 | }, 110 | "required": [ 111 | "info" 112 | ] 113 | } 114 | -------------------------------------------------------------------------------- /cmd/aidoku/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var ( 11 | Verbose bool 12 | ForceColor bool 13 | 14 | version = "develop" 15 | commit string 16 | date string 17 | builtBy string 18 | ) 19 | 20 | var rootCmd = &cobra.Command{ 21 | Use: "aidoku", 22 | Short: "Aidoku development toolkit", 23 | } 24 | 25 | func Execute() { 26 | if err := rootCmd.Execute(); err != nil { 27 | fmt.Println(err) 28 | os.Exit(1) 29 | } 30 | } 31 | 32 | func init() { 33 | rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output") 34 | rootCmd.PersistentFlags().BoolVar(&ForceColor, "force-color", false, "always output with color") 35 | rootCmd.CompletionOptions.HiddenDefaultCmd = true 36 | 37 | formattedVersion := FormatVersion(version, commit, date, builtBy) 38 | rootCmd.SetVersionTemplate(formattedVersion) 39 | rootCmd.Version = formattedVersion 40 | 41 | rootCmd.AddCommand(NewVersionCmd(version, commit, date, builtBy)) 42 | } 43 | -------------------------------------------------------------------------------- /cmd/aidoku/cmd/serve.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/Aidoku/aidoku-cli/internal/build" 12 | "github.com/Aidoku/aidoku-cli/internal/common" 13 | "github.com/fatih/color" 14 | "github.com/felixge/httpsnoop" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | var ( 19 | cyan = color.New(color.FgCyan).SprintFunc() 20 | red = color.New(color.FgRed).SprintFunc() 21 | ) 22 | 23 | var serveCmd = &cobra.Command{ 24 | Use: "serve ", 25 | Short: "Build a source list and serve it on the local network", 26 | Version: rootCmd.Version, 27 | Args: cobra.MinimumNArgs(1), 28 | SilenceUsage: true, 29 | SilenceErrors: true, 30 | RunE: func(cmd *cobra.Command, args []string) error { 31 | c := make(chan os.Signal) 32 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 33 | go func() { 34 | <-c 35 | os.Exit(0) 36 | }() 37 | 38 | if ForceColor { 39 | color.NoColor = false 40 | } 41 | 42 | address, _ := cmd.Flags().GetString("address") 43 | output, _ := cmd.Flags().GetString("output") 44 | port, _ := cmd.Flags().GetString("port") 45 | 46 | build.BuildWrapper(args, output, false, build.WebTemplateArguments{}) 47 | 48 | fmt.Println("Listening on these addresses:") 49 | if address == "0.0.0.0" { 50 | common.PrintAddresses(port) 51 | } else { 52 | color.Green(" http://%s:%s", address, port) 53 | } 54 | fmt.Println("Hit CTRL-C to stop the server") 55 | 56 | handler := http.FileServer(http.Dir(output)) 57 | http.Handle("/", handler) 58 | wrappedHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 59 | timestamp := time.Now().UTC().Format(time.RFC3339) 60 | method := r.Method 61 | url := r.URL 62 | userAgent := r.UserAgent() 63 | fmt.Printf("[%s] \"%s %s\" \"%s\"\n", timestamp, cyan(method), cyan(url), userAgent) 64 | 65 | m := httpsnoop.CaptureMetrics(handler, w, r) 66 | timestamp = time.Now().UTC().Format(time.RFC3339) 67 | statusCode := m.Code 68 | if statusCode >= 400 { 69 | fmt.Printf("[%s] \"%s %s\" Error (%s): \"%s\"\n", timestamp, red(method), red(url), red(statusCode), red(http.StatusText(statusCode))) 70 | } 71 | }) 72 | return http.ListenAndServe(address+":"+port, wrappedHandler) 73 | }, 74 | } 75 | 76 | func init() { 77 | rootCmd.AddCommand(serveCmd) 78 | serveCmd.Flags().StringP("address", "a", "0.0.0.0", "Address to broadcast source list") 79 | serveCmd.Flags().StringP("port", "p", "8080", "The port to broadcast the source list on") 80 | serveCmd.Flags().StringP("output", "o", "public", "The source list folder") 81 | 82 | serveCmd.MarkZshCompPositionalArgumentFile(1, "*.aix") 83 | serveCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 84 | return []string{"aix"}, cobra.ShellCompDirectiveFilterFileExt 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /cmd/aidoku/cmd/verify.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "archive/zip" 5 | "errors" 6 | "fmt" 7 | "image" 8 | _ "image/png" 9 | "io" 10 | "strings" 11 | 12 | "github.com/Aidoku/aidoku-cli/internal/common" 13 | rice "github.com/GeertJohan/go.rice" 14 | "github.com/fatih/color" 15 | "github.com/spf13/cobra" 16 | "github.com/xeipuuv/gojsonschema" 17 | ) 18 | 19 | func verifySchemas(schema gojsonschema.JSONLoader, f *zip.File) error { 20 | rc, err := f.Open() 21 | if err != nil { 22 | color.Red("error: couldn't read %s: %s", f.Name, err) 23 | return err 24 | } 25 | buf := new(strings.Builder) 26 | io.Copy(buf, rc) 27 | document := gojsonschema.NewStringLoader(buf.String()) 28 | result, err := gojsonschema.Validate(schema, document) 29 | if err != nil { 30 | color.Yellow("warning: could not verify %s: %s", f.Name, err) 31 | return err 32 | } 33 | if !result.Valid() { 34 | color.Red("no") 35 | for _, desc := range result.Errors() { 36 | fmt.Printf(" * %s\n", desc) 37 | } 38 | return errors.New("invalid") 39 | } 40 | return nil 41 | } 42 | 43 | func opaque(im image.Image) bool { 44 | if oim, yes := im.(interface { 45 | Opaque() bool 46 | }); yes { 47 | return oim.Opaque() 48 | } 49 | 50 | rect := im.Bounds() 51 | for y := rect.Min.Y; y < rect.Max.Y; y++ { 52 | for x := rect.Min.X; x < rect.Max.X; x++ { 53 | if _, _, _, a := im.At(x, y).RGBA(); a != 0xffff { 54 | return false 55 | } 56 | } 57 | 58 | } 59 | return true 60 | } 61 | 62 | var verifyCmd = &cobra.Command{ 63 | Use: "verify ", 64 | Short: "Test Aidyesu packages if they're ready for publishing", 65 | Version: rootCmd.Version, 66 | Args: cobra.MinimumNArgs(1), 67 | SilenceUsage: true, 68 | SilenceErrors: true, 69 | RunE: func(cmd *cobra.Command, args []string) error { 70 | if ForceColor { 71 | color.NoColor = false 72 | } 73 | 74 | zipFiles := common.ProcessGlobs(args) 75 | 76 | box := rice.MustFindBox("resources") 77 | filterSchema := gojsonschema.NewStringLoader(box.MustString("schemas/filters.schema.json")) 78 | sourceSchema := gojsonschema.NewStringLoader(box.MustString("schemas/source.schema.json")) 79 | settingsSchema := gojsonschema.NewStringLoader(box.MustString("schemas/settings.schema.json")) 80 | 81 | errored := false 82 | 83 | for _, file := range zipFiles { 84 | r, err := zip.OpenReader(file) 85 | if err != nil { 86 | color.Red("error: %s is not a valid zip file", file) 87 | continue 88 | } 89 | defer r.Close() 90 | 91 | hasMainWasm := false 92 | hasSourceJson := false 93 | hasIcon := false 94 | iconSizeValid := false 95 | iconOpaque := false 96 | sourceJsonValid := false 97 | filterJsonValid := true 98 | settingJsonValid := true 99 | fmt.Printf("* Testing %s\n", file) 100 | for _, f := range r.File { 101 | if f.Name == "Payload/" { 102 | continue 103 | } 104 | fmt.Printf(" * %s\n", strings.TrimPrefix(f.Name, "Payload/")) 105 | if f.Name == "Payload/main.wasm" { 106 | hasMainWasm = true 107 | // TODO: Check if there are enough exported functions 108 | fmt.Println(" * note: `aidoku verify` cannot check if the executable is valid") 109 | } else if f.Name == "Payload/Icon.png" { 110 | hasIcon = true 111 | rc, err := f.Open() 112 | if err != nil { 113 | color.Red(" * error: couldn't read image file for %s: %s", file, err) 114 | continue 115 | } 116 | m, _, err := image.Decode(rc) 117 | if err != nil { 118 | color.Red(" * error: could not decode image file for %s: %s", file, err) 119 | continue 120 | } 121 | fmt.Printf(" * dimensions are 128x128... ") 122 | bounds := m.Bounds() 123 | w := bounds.Dx() 124 | h := bounds.Dy() 125 | if w != 128 && h != 128 { 126 | color.Red("error: expected 128x128, found %dx%d", w, h) 127 | } else { 128 | color.Green("yes") 129 | iconSizeValid = true 130 | } 131 | 132 | fmt.Printf(" * is fully opaque... ") 133 | if !opaque(m) { 134 | color.Red("no") 135 | } else { 136 | color.Green("yes") 137 | iconOpaque = true 138 | } 139 | 140 | } else if f.Name == "Payload/source.json" { 141 | hasSourceJson = true 142 | fmt.Printf(" * is valid against schema... ") 143 | err = verifySchemas(sourceSchema, f) 144 | if err == nil { 145 | sourceJsonValid = true 146 | color.Green("yes") 147 | continue 148 | } 149 | } else if f.Name == "Payload/settings.json" { 150 | fmt.Printf(" * is valid against schema... ") 151 | err = verifySchemas(settingsSchema, f) 152 | if err != nil { 153 | settingJsonValid = false 154 | continue 155 | } 156 | color.Green("yes") 157 | } else if f.Name == "Payload/filters.json" { 158 | fmt.Printf(" * is valid against schema... ") 159 | err = verifySchemas(filterSchema, f) 160 | if err != nil { 161 | filterJsonValid = false 162 | continue 163 | } 164 | color.Green("yes") 165 | } 166 | } 167 | if !(hasMainWasm && hasSourceJson && hasIcon && iconSizeValid && iconOpaque && sourceJsonValid && settingJsonValid && filterJsonValid) { 168 | if !hasMainWasm { 169 | color.Red(" * test failed: did not find main.wasm") 170 | } 171 | if !hasSourceJson { 172 | color.Red(" * test failed: did not find source.json") 173 | } 174 | if !hasIcon { 175 | color.Red(" * test failed: did not find Icon.png") 176 | } 177 | errored = true 178 | } 179 | fmt.Printf("\n") 180 | } 181 | 182 | if errored { 183 | return errors.New("one or more packages failed validation, see above") 184 | } else { 185 | return nil 186 | } 187 | }, 188 | } 189 | 190 | func init() { 191 | rootCmd.AddCommand(verifyCmd) 192 | 193 | buildCmd.MarkZshCompPositionalArgumentFile(1, "*.aix") 194 | buildCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 195 | return []string{"aix"}, cobra.ShellCompDirectiveFilterFileExt 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /cmd/aidoku/cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func NewVersionCmd(version, commit, date, builtBy string) *cobra.Command { 11 | return &cobra.Command{ 12 | Use: "version", 13 | Short: "Print version", 14 | Run: func(cmd *cobra.Command, args []string) { 15 | fmt.Println(FormatVersion(version, commit, date, builtBy)) 16 | }, 17 | } 18 | } 19 | 20 | func FormatVersion(version, commit, date, builtBy string) string { 21 | var output strings.Builder 22 | output.WriteString("aidoku-cli version ") 23 | output.WriteString(strings.TrimPrefix(version, "v")) 24 | if commit != "" { 25 | output.WriteString(", commit ") 26 | output.WriteString(commit) 27 | } 28 | if date != "" { 29 | output.WriteString(", built at ") 30 | output.WriteString(date) 31 | } 32 | if builtBy != "" { 33 | output.WriteString(", built by ") 34 | output.WriteString(builtBy) 35 | } 36 | return output.String() 37 | } 38 | -------------------------------------------------------------------------------- /cmd/aidoku/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/Aidoku/aidoku-cli/cmd/aidoku/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Aidoku/aidoku-cli 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/AlecAivazis/survey/v2 v2.3.4 7 | github.com/GeertJohan/go.rice v1.0.2 8 | github.com/bmatcuk/doublestar/v4 v4.0.2 9 | github.com/fatih/color v1.13.0 10 | github.com/felixge/httpsnoop v1.0.3 11 | github.com/iancoleman/strcase v0.2.0 12 | github.com/segmentio/fasthash v1.0.3 13 | github.com/spf13/cobra v1.4.0 14 | github.com/valyala/fastjson v1.6.3 15 | github.com/xeipuuv/gojsonschema v1.2.0 16 | golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf 17 | golang.org/x/text v0.3.8 18 | ) 19 | 20 | require ( 21 | github.com/daaku/go.zipexe v1.0.1 // indirect 22 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 23 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 24 | github.com/mattn/go-colorable v0.1.12 // indirect 25 | github.com/mattn/go-isatty v0.0.14 // indirect 26 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 27 | github.com/spf13/pflag v1.0.5 // indirect 28 | github.com/stretchr/testify v1.7.0 // indirect 29 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 30 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 31 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect 32 | golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect 33 | gopkg.in/yaml.v3 v3.0.0 // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/AlecAivazis/survey/v2 v2.3.4 h1:pchTU9rsLUSvWEl2Aq9Pv3k0IE2fkqtGxazskAMd9Ng= 2 | github.com/AlecAivazis/survey/v2 v2.3.4/go.mod h1:hrV6Y/kQCLhIZXGcriDCUBtB3wnN7156gMXJ3+b23xM= 3 | github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= 4 | github.com/GeertJohan/go.rice v1.0.2 h1:PtRw+Tg3oa3HYwiDBZyvOJ8LdIyf6lAovJJtr7YOAYk= 5 | github.com/GeertJohan/go.rice v1.0.2/go.mod h1:af5vUNlDNkCjOZeSGFgIJxDje9qdjsO6hshx0gTmZt4= 6 | github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= 7 | github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= 8 | github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= 9 | github.com/bmatcuk/doublestar/v4 v4.0.2 h1:X0krlUVAVmtr2cRoTqR8aDMrDqnB36ht8wpWTiQ3jsA= 10 | github.com/bmatcuk/doublestar/v4 v4.0.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= 11 | github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 12 | github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= 13 | github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 14 | github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= 15 | github.com/daaku/go.zipexe v1.0.1 h1:wV4zMsDOI2SZ2m7Tdz1Ps96Zrx+TzaK15VbUaGozw0M= 16 | github.com/daaku/go.zipexe v1.0.1/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8= 17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 21 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 22 | github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= 23 | github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 24 | github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= 25 | github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= 26 | github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= 27 | github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= 28 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 29 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 30 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 31 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 32 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 33 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 34 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 35 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 36 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 37 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 38 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 39 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 40 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 41 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 42 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= 43 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 44 | github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc= 45 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 46 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 47 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 48 | github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= 49 | github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= 50 | github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= 51 | github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= 52 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 53 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 54 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 55 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 56 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 57 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 58 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 59 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 60 | github.com/valyala/fastjson v1.6.3 h1:tAKFnnwmeMGPbwJ7IwxcTPCNr3uIzoIj3/Fh90ra4xc= 61 | github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= 62 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 63 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 64 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= 65 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 66 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 67 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 68 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= 69 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 70 | golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf h1:oXVg4h2qJDd9htKxb5SCpFBHLipW6hXmL3qpUixS2jw= 71 | golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf/go.mod h1:yh0Ynu2b5ZUe3MQfp2nM0ecK7wsgouWTDN0FNeJuIys= 72 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 73 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 74 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 75 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 76 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 78 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= 79 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 80 | golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= 81 | golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8= 82 | golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 83 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 84 | golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= 85 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 86 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 87 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 88 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 89 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 90 | gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= 91 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 92 | -------------------------------------------------------------------------------- /internal/build/build.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | import ( 4 | "archive/zip" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "html/template" 9 | "io" 10 | "os" 11 | "sort" 12 | "strings" 13 | "sync" 14 | 15 | "github.com/Aidoku/aidoku-cli/internal/common" 16 | rice "github.com/GeertJohan/go.rice" 17 | "github.com/fatih/color" 18 | "github.com/segmentio/fasthash/fnv1a" 19 | "github.com/valyala/fastjson" 20 | ) 21 | 22 | type source struct { 23 | Id string `json:"id"` 24 | Name string `json:"name"` 25 | File string `json:"file"` 26 | Icon string `json:"icon"` 27 | Lang string `json:"lang"` 28 | Version int `json:"version"` 29 | NSFW int `json:"nsfw"` 30 | MinVersion string `json:"minAppVersion,omitempty"` 31 | MaxVersion string `json:"maxAppVersion,omitempty"` 32 | } 33 | 34 | type WebTemplateArguments struct { 35 | Title string 36 | Description string 37 | Icon string 38 | } 39 | 40 | func BuildWrapper(zipPatterns []string, output string, web bool, webArgs WebTemplateArguments) error { 41 | os.RemoveAll(output) 42 | fileList := common.ProcessGlobs(zipPatterns) 43 | if len(fileList) == 0 { 44 | return errors.New("no files given") 45 | } 46 | err := os.MkdirAll(output, os.FileMode(0777)) 47 | if err != nil { 48 | color.Red("fatal: could not create output folder") 49 | return err 50 | } 51 | os.MkdirAll(output+"/icons", os.FileMode(0777)) 52 | os.MkdirAll(output+"/sources", os.FileMode(0777)) 53 | 54 | err = BuildSource(fileList, output) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | if web { 60 | err = BuildWeb(webArgs, output) 61 | if err != nil { 62 | return err 63 | } 64 | } 65 | 66 | return nil 67 | } 68 | 69 | func BuildWeb(args WebTemplateArguments, output string) error { 70 | box := rice.MustFindBox("web") 71 | 72 | bytes := box.MustBytes("index.html.tmpl") 73 | 74 | tmpl, err := template.New("index").Parse(string(bytes)) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | file, err := os.Create(output + "/index.html") 80 | if err != nil { 81 | return err 82 | } 83 | 84 | err = tmpl.Execute(file, args) 85 | if err != nil { 86 | return err 87 | } 88 | return nil 89 | } 90 | 91 | func BuildSource(zipFiles []string, output string) error { 92 | var wg sync.WaitGroup 93 | sourceList := struct { 94 | sync.Mutex 95 | data []source 96 | }{} 97 | sourceIds := sync.Map{} 98 | for _, file := range zipFiles { 99 | wg.Add(1) 100 | go func(zipFile string) { 101 | defer wg.Done() 102 | r, err := zip.OpenReader(zipFile) 103 | if err != nil { 104 | color.Red("error: %s is not a valid package", zipFile) 105 | return 106 | } 107 | defer r.Close() 108 | 109 | var sourceInfo source 110 | var parser fastjson.Parser 111 | hasIcon := false 112 | zipFileHash := fmt.Sprintf("%x", fnv1a.HashString64(zipFile)) 113 | tempImageFile := fmt.Sprintf("%s/icons/%s.png", output, zipFileHash) 114 | for _, f := range r.File { 115 | if f.Name == "Payload/source.json" { 116 | rc, err := f.Open() 117 | if err != nil { 118 | color.Red("error: couldn't read source info for %s: %s", zipFile, err) 119 | os.Remove(tempImageFile) 120 | return 121 | } 122 | buf := new(strings.Builder) 123 | io.Copy(buf, rc) 124 | 125 | raw, err := parser.Parse(buf.String()) 126 | if err != nil { 127 | color.Red("error: source.json is malformed for %s: %s", zipFile, err) 128 | os.Remove(tempImageFile) 129 | return 130 | } 131 | 132 | info := raw.Get("info") 133 | sourceInfo.Id = string(info.GetStringBytes("id")) 134 | if val, ok := sourceIds.Load(sourceInfo.Id); ok { 135 | color.Red("error: duplicate source identifier %s in %s, first found in %s", sourceInfo.Id, zipFile, val) 136 | os.Remove(tempImageFile) 137 | return 138 | } 139 | sourceIds.Store(sourceInfo.Id, zipFile) 140 | 141 | sourceInfo.Lang = string(info.GetStringBytes("lang")) 142 | sourceInfo.Name = string(info.GetStringBytes("name")) 143 | sourceInfo.Version = info.GetInt("version") 144 | sourceInfo.NSFW = info.GetInt("nsfw") 145 | sourceInfo.File = fmt.Sprintf("%s-v%d.aix", sourceInfo.Id, sourceInfo.Version) 146 | sourceInfo.Icon = fmt.Sprintf("%s-v%d.png", sourceInfo.Id, sourceInfo.Version) 147 | if minVersion := info.GetStringBytes("minAppVersion"); minVersion != nil { 148 | sourceInfo.MinVersion = string(minVersion) 149 | } 150 | if maxVersion := info.GetStringBytes("maxAppVersion"); maxVersion != nil { 151 | sourceInfo.MaxVersion = string(maxVersion) 152 | } 153 | 154 | common.CopyFileContents(zipFile, output+"/sources/"+sourceInfo.File) 155 | sourceList.Lock() 156 | sourceList.data = append(sourceList.data, sourceInfo) 157 | sourceList.Unlock() 158 | } else if f.Name == "Payload/Icon.png" { 159 | hasIcon = true 160 | rc, err := f.Open() 161 | if err != nil { 162 | color.Red("error: couldn't read icon for %s", zipFile) 163 | return 164 | } 165 | img, err := os.Create(tempImageFile) 166 | if err != nil { 167 | color.Red("error: Couldn't create temporary icon file %s/icons/%s.png: %s", output, zipFileHash, err) 168 | hasIcon = false 169 | return 170 | } 171 | io.Copy(img, rc) 172 | img.Sync() 173 | img.Close() 174 | } 175 | } 176 | imageFile := fmt.Sprintf("%s/icons/%s", output, sourceInfo.Icon) 177 | if !hasIcon { 178 | color.Yellow("warning: %s doesn't have an icon, generating placeholder", zipFile) 179 | err = common.GeneratePng(imageFile) 180 | if err != nil { 181 | return 182 | } 183 | 184 | } else { 185 | os.Rename(tempImageFile, imageFile) 186 | } 187 | }(file) 188 | } 189 | wg.Wait() 190 | 191 | sort.Slice(sourceList.data, func(i, j int) bool { 192 | return sourceList.data[i].Id < sourceList.data[j].Id 193 | }) 194 | 195 | b, err := json.Marshal(sourceList.data) 196 | if err != nil { 197 | color.Red("fatal: couldn't serialize source list: %s", err.Error()) 198 | return err 199 | } 200 | 201 | fm, err := os.Create(output + "/index.min.json") 202 | if err != nil { 203 | return err 204 | } 205 | fm.Write(b) 206 | fm.Sync() 207 | fm.Close() 208 | 209 | b, err = json.MarshalIndent(sourceList.data, "", " ") 210 | if err != nil { 211 | color.Red("fatal: couldn't serialize source list: %s", err.Error()) 212 | return err 213 | } 214 | 215 | f, err := os.Create(output + "/index.json") 216 | if err != nil { 217 | return err 218 | } 219 | f.Write(b) 220 | f.Sync() 221 | f.Close() 222 | return nil 223 | } 224 | -------------------------------------------------------------------------------- /internal/build/web/index.html.tmpl: -------------------------------------------------------------------------------- 1 | {{ if ne (len .Title) 0 }}{{ .Title }}{{ end }}{{ if ne (len .Description) 0 }}{{ end }}{{ if ne (len .Icon) 0 }}{{ end }}
{{ if ne (len .Title) 0 }}

{{ .Title }}

{{ end }}Add to Aidoku
On a device with Aidoku installed, tap Add to Aidoku or use the base URL to add this source list.

Base URL:

Sources

2 | -------------------------------------------------------------------------------- /internal/common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "fmt" 7 | "io" 8 | "net" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/bmatcuk/doublestar/v4" 14 | "github.com/fatih/color" 15 | ) 16 | 17 | func CopyFileContents(src, dst string) (err error) { 18 | in, err := os.Open(src) 19 | if err != nil { 20 | return 21 | } 22 | defer in.Close() 23 | out, err := os.Create(dst) 24 | if err != nil { 25 | return 26 | } 27 | defer func() { 28 | cerr := out.Close() 29 | if err == nil { 30 | err = cerr 31 | } 32 | }() 33 | if _, err = io.Copy(out, in); err != nil { 34 | return 35 | } 36 | err = out.Sync() 37 | return 38 | } 39 | 40 | func isPrivateIP(ip net.IP) bool { 41 | var privateIPBlocks []*net.IPNet 42 | for _, cidr := range []string{ 43 | "10.0.0.0/8", // RFC1918 44 | "172.16.0.0/12", // RFC1918 45 | "192.168.0.0/16", // RFC1918 46 | } { 47 | _, block, _ := net.ParseCIDR(cidr) 48 | privateIPBlocks = append(privateIPBlocks, block) 49 | } 50 | 51 | for _, block := range privateIPBlocks { 52 | if block.Contains(ip) { 53 | return true 54 | } 55 | } 56 | 57 | return false 58 | } 59 | 60 | func PrintAddresses(port string) { 61 | ifaces, _ := net.Interfaces() 62 | for _, i := range ifaces { 63 | addrs, _ := i.Addrs() 64 | for _, addr := range addrs { 65 | var ip net.IP 66 | switch v := addr.(type) { 67 | case *net.IPNet: 68 | ip = v.IP 69 | case *net.IPAddr: 70 | ip = v.IP 71 | } 72 | if ip != nil { 73 | if isPrivateIP(ip) { 74 | color.Green(" http://%s:%s\n", ip.String(), port) 75 | } else if !strings.Contains(ip.String(), ":") { 76 | fmt.Printf(" http://%s:%s\n", ip.String(), port) 77 | } 78 | } 79 | } 80 | } 81 | color.Green(" http://127.0.0.1:%s\n", port) 82 | } 83 | 84 | func GeneratePng(location string) error { 85 | img, err := os.Create(location) 86 | if err != nil { 87 | color.Red("error: Couldn't write icon file %s: %s", location, err.Error()) 88 | return err 89 | } 90 | transparent, _ := base64.StdEncoding.DecodeString("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg==") 91 | io.Copy(img, bytes.NewReader(transparent)) 92 | img.Sync() 93 | img.Close() 94 | return nil 95 | } 96 | 97 | func ProcessGlobs(globs []string) []string { 98 | var fileList []string 99 | for _, arg := range globs { 100 | base, pattern := doublestar.SplitPattern(filepath.ToSlash(arg)) 101 | fsys := os.DirFS(base) 102 | files, err := doublestar.Glob(fsys, pattern) 103 | if err != nil { 104 | color.Red("error: invalid glob pattern %s", arg) 105 | continue 106 | } 107 | for _, file := range files { 108 | fileList = append(fileList, base+"/"+file) 109 | } 110 | } 111 | return fileList 112 | } 113 | -------------------------------------------------------------------------------- /internal/templates/resources/common/res/filters.json.tmpl: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "title" 4 | }, 5 | { 6 | "type": "author" 7 | }, 8 | { 9 | "type": "group", 10 | "name": "A group filter", 11 | "filters": [ 12 | { 13 | "type": "check", 14 | "name": "A checkbox", 15 | "default": true 16 | } 17 | ] 18 | }, 19 | { 20 | "type": "group", 21 | "name": "Another group filter", 22 | "filters": [ 23 | { 24 | "type": "genre", 25 | "name": "A genre", 26 | "canExclude": true 27 | } 28 | ] 29 | }, 30 | { 31 | "type": "select", 32 | "name": "A select filter", 33 | "options": [ 34 | 35 | ] 36 | }, 37 | { 38 | "type": "sort", 39 | "name": "A sorter", 40 | "canAscend": true, 41 | "options": [ 42 | 43 | ], 44 | "default": { 45 | "index": 0, 46 | "ascending": false 47 | } 48 | } 49 | ] 50 | -------------------------------------------------------------------------------- /internal/templates/resources/common/res/settings.json.tmpl: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "group", 4 | "title": "Settings", 5 | "footer": "You can have footers for a setting group", 6 | "items": [ 7 | { 8 | "type": "select", 9 | "key": "something", 10 | "title": "A select option", 11 | "values": ["your", "values", "here"], 12 | "titles": ["your", "titles", "here"], 13 | "default": "here" 14 | }, 15 | { 16 | "type": "switch", 17 | "key": "something2", 18 | "title": "A switch", 19 | "subtitle": "A subtext to describe this option", 20 | "default": false 21 | }, 22 | { 23 | "type": "text", 24 | "key": "something3", 25 | "placeholder": "A text box" 26 | } 27 | ] 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /internal/templates/resources/common/res/source.json.tmpl: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "id": "{{ .Language }}.{{ .Name | ToLower | SlugifyAs }}", 4 | "lang": "{{ .Language }}", 5 | "name": "{{ .Name }}", 6 | "version": 1, 7 | "url": "{{ .Homepage }}", 8 | "nsfw": {{ .Nsfw }} 9 | },{{ if eq .Language "multi" }} 10 | "languages": [ 11 | 12 | ], 13 | {{ end }} 14 | "listings": [ 15 | 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /internal/templates/resources/rust-template/Cargo.toml.tmpl: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["template", "sources/*"] 3 | 4 | [profile.dev] 5 | panic = "abort" 6 | 7 | [profile.release] 8 | panic = "abort" 9 | opt-level = "s" 10 | strip = true 11 | lto = true 12 | -------------------------------------------------------------------------------- /internal/templates/resources/rust-template/build.ps1.tmpl: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Template source build script for Windows 4 | #> 5 | #requires -version 5 6 | [cmdletbinding()] 7 | param ( 8 | [Parameter(ParameterSetName="help", Mandatory)] 9 | [alias('h')] 10 | [switch]$help, 11 | 12 | [Parameter(ParameterSetName="all", Mandatory)] 13 | [alias('a')] 14 | [switch]$all, 15 | 16 | [Parameter(Position=0, ParameterSetName="some", Mandatory)] 17 | [alias('s')] 18 | [string[]]$sources 19 | ) 20 | 21 | function Package-Source { 22 | param ( 23 | [Parameter(Mandatory = $true, Position = 0)] 24 | [String[]]$Name, 25 | 26 | [switch]$Build 27 | ) 28 | $Name | ForEach-Object { 29 | $source = $_ 30 | if ($Build) { 31 | Write-Output "building $source" 32 | Set-Location ./sources/$source 33 | cargo +nightly build --release 34 | Set-Location ../.. 35 | } 36 | 37 | Write-Output "packaging $source" 38 | New-Item -ItemType Directory -Path target/wasm32-unknown-unknown/release/Payload -Force | Out-Null 39 | Copy-Item res/* target/wasm32-unknown-unknown/release/Payload -ErrorAction SilentlyContinue 40 | Copy-Item sources/$source/res/* target/wasm32-unknown-unknown/release/Payload -ErrorAction SilentlyContinue 41 | Set-Location target/wasm32-unknown-unknown/release 42 | Copy-Item "$source.wasm" Payload/main.wasm 43 | Compress-Archive -Force -Path Payload -DestinationPath "../../../$source.aix" 44 | Remove-Item -Recurse -Force Payload/ 45 | Set-Location ../../.. 46 | } 47 | } 48 | 49 | if ($help -or ($null -eq $PSBoundParameters.Keys)) { 50 | Get-Help $MyInvocation.MyCommand.Path -Detailed 51 | break 52 | } 53 | 54 | if ($all) { 55 | cargo +nightly build --release 56 | Get-ChildItem ./sources | ForEach-Object { 57 | $source = (Split-Path -Leaf $_) 58 | Package-Source $source 59 | } 60 | } else { 61 | $sources | ForEach-Object { 62 | Package-Source $_ -Build 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /internal/templates/resources/rust-template/build.sh.tmpl: -------------------------------------------------------------------------------- 1 | # template source build script 2 | # usage: ./build.sh [source_name/-a] 3 | 4 | if [ "$1" != "-a" ]; then 5 | # compile specified source 6 | cargo +nightly build --release 7 | 8 | echo "packaging $1"; 9 | mkdir -p target/wasm32-unknown-unknown/release/Payload 10 | cp res/* target/wasm32-unknown-unknown/release/Payload 11 | cp sources/$1/res/* target/wasm32-unknown-unknown/release/Payload 12 | cd target/wasm32-unknown-unknown/release 13 | cp $1.wasm Payload/main.wasm 14 | zip -r $1.aix Payload 15 | mv $1.aix ../../../$1.aix 16 | rm -rf Payload 17 | else 18 | # compile all sources 19 | cargo +nightly build --release 20 | 21 | for dir in sources/*/ 22 | do 23 | dir=${dir%*/} 24 | dir=${dir##*/} 25 | echo "packaging $dir"; 26 | 27 | mkdir -p target/wasm32-unknown-unknown/release/Payload 28 | cp res/* target/wasm32-unknown-unknown/release/Payload 29 | cp sources/$dir/res/* target/wasm32-unknown-unknown/release/Payload 30 | cd target/wasm32-unknown-unknown/release 31 | cp $dir.wasm Payload/main.wasm 32 | zip -r $dir.aix Payload >> /dev/null 33 | mv $dir.aix ../../../$dir.aix 34 | rm -rf Payload 35 | cd ../../../ 36 | done 37 | fi 38 | -------------------------------------------------------------------------------- /internal/templates/resources/rust-template/template/Cargo.toml.tmpl: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "{{ .Name | ToLower | SlugifyRust }}_template" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | aidoku = { git = "https://github.com/Aidoku/aidoku-rs/", features = ["helpers"] } 9 | -------------------------------------------------------------------------------- /internal/templates/resources/rust-template/template/src/lib.rs.tmpl: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | pub mod helper; 3 | pub mod template; 4 | -------------------------------------------------------------------------------- /internal/templates/resources/rust-template/template/src/template.rs.tmpl: -------------------------------------------------------------------------------- 1 | use aidoku::{ 2 | error::Result, 3 | prelude::*, 4 | std::{String, Vec, net::Request}, 5 | helpers, 6 | Manga, MangaPageResult, Page, Chapter, DeepLink 7 | }; 8 | 9 | pub fn get_manga_list(todo!()) -> Result { 10 | todo!() 11 | } 12 | 13 | pub fn get_manga_listing(todo!()) -> Result { 14 | todo!() 15 | } 16 | 17 | pub fn get_manga_details(todo!()) -> Result { 18 | todo!() 19 | } 20 | 21 | pub fn get_chapter_list(todo!()) -> Result> { 22 | todo!() 23 | } 24 | 25 | pub fn get_page_list(todo!()) -> Result> { 26 | todo!() 27 | } 28 | 29 | pub fn modify_image_request(todo!(), request: Request) { 30 | todo!() 31 | } 32 | 33 | pub fn handle_url(todo!()) -> Result { 34 | todo!() 35 | } 36 | -------------------------------------------------------------------------------- /internal/templates/resources/rust/.cargo/config.tmpl: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-unknown-unknown" 3 | -------------------------------------------------------------------------------- /internal/templates/resources/rust/Cargo.toml.tmpl: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "{{ .Name | ToLower | SlugifyRust }}" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | {{ if eq (len .TemplateName) 0 }} 9 | [profile.dev] 10 | panic = "abort" 11 | 12 | [profile.release] 13 | panic = "abort" 14 | opt-level = "s" 15 | strip = true 16 | lto = true 17 | {{ end }} 18 | [dependencies] 19 | aidoku = { git = "https://github.com/Aidoku/aidoku-rs", features = ["helpers"] }{{ if not (eq (len .TemplateName) 0) }} 20 | {{ .TemplateName }} = { path = "../../template" }{{ end }} 21 | -------------------------------------------------------------------------------- /internal/templates/resources/rust/build.ps1.tmpl: -------------------------------------------------------------------------------- 1 | function Package-Source { 2 | param ( 3 | [Parameter(Mandatory = $true, Position = 0)] 4 | [String[]]$Name, 5 | [switch]$Build 6 | ) 7 | $Name | ForEach-Object { 8 | $source = $_ 9 | if ($Build) { 10 | Write-Output "building $source" 11 | cargo +nightly build --release 12 | } 13 | Write-Output "packaging $source" 14 | New-Item -ItemType Directory -Path target/wasm32-unknown-unknown/release/Payload -Force | Out-Null 15 | Copy-Item res/* target/wasm32-unknown-unknown/release/Payload -ErrorAction SilentlyContinue 16 | Set-Location target/wasm32-unknown-unknown/release 17 | Copy-Item *.wasm Payload/main.wasm 18 | Compress-Archive -Force -DestinationPath "../../../package.aix" -Path Payload 19 | Remove-Item -Recurse -Force Payload/ 20 | Set-Location ../../.. 21 | } 22 | } 23 | Package-Source {{ .Name | ToLower | SlugifyRust }} -Build 24 | -------------------------------------------------------------------------------- /internal/templates/resources/rust/build.sh.tmpl: -------------------------------------------------------------------------------- 1 | cargo +nightly build --release 2 | mkdir -p target/wasm32-unknown-unknown/release/Payload 3 | cp res/* target/wasm32-unknown-unknown/release/Payload 4 | cp target/wasm32-unknown-unknown/release/*.wasm target/wasm32-unknown-unknown/release/Payload/main.wasm 5 | cd target/wasm32-unknown-unknown/release ; zip -r package.aix Payload 6 | mv package.aix ../../../package.aix 7 | -------------------------------------------------------------------------------- /internal/templates/resources/rust/src/lib.rs.tmpl: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | use aidoku::{ 3 | error::Result, 4 | prelude::*, 5 | std::{String, Vec, net::Request}, 6 | helpers, 7 | Chapter, Filter, Listing, Manga, MangaPageResult, Page, DeepLink 8 | }; 9 | 10 | #[get_manga_list] 11 | fn get_manga_list(filters: Vec, page: i32) -> Result { 12 | todo!() 13 | } 14 | 15 | #[get_manga_listing] 16 | fn get_manga_listing(listing: Listing, page: i32) -> Result { 17 | todo!() 18 | } 19 | 20 | #[get_manga_details] 21 | fn get_manga_details(id: String) -> Result { 22 | todo!() 23 | } 24 | 25 | #[get_chapter_list] 26 | fn get_chapter_list(id: String) -> Result> { 27 | todo!() 28 | } 29 | 30 | #[get_page_list] 31 | fn get_page_list(id: String) -> Result> { 32 | todo!() 33 | } 34 | 35 | #[modify_image_request] 36 | fn modify_image_request(request: Request) { 37 | todo!() 38 | } 39 | 40 | #[handle_url] 41 | fn handle_url(url: String) -> Result { 42 | todo!() 43 | } 44 | -------------------------------------------------------------------------------- /internal/templates/rust-template.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "path" 7 | "strings" 8 | ) 9 | 10 | func RustTemplateGenerator(output string, source Source) error { 11 | err := GenerateCommon(output, source) 12 | if err != nil { 13 | return err 14 | } 15 | os.MkdirAll(output+"/.cargo", os.FileMode(0754)) 16 | os.MkdirAll(output+"/sources", os.FileMode(0754)) 17 | os.MkdirAll(output+"/template/src", os.FileMode(0754)) 18 | os.Remove(output + "/src") 19 | os.RemoveAll(output + "/res") 20 | 21 | files := map[string]func() []byte{ 22 | "/Cargo.toml": templateFactory(box, "rust-template/Cargo.toml.tmpl"), 23 | "/build.sh": templateFactory(box, "rust-template/build.sh.tmpl"), 24 | "/build.ps1": templateFactory(box, "rust-template/build.ps1.tmpl"), 25 | "/.cargo/config": templateFactory(box, "rust/.cargo/config.tmpl"), 26 | "/template/Cargo.toml": templateFactory(box, "rust-template/template/Cargo.toml.tmpl"), 27 | "/template/src/lib.rs": templateFactory(box, "rust-template/template/src/lib.rs.tmpl"), 28 | "/template/src/template.rs": templateFactory(box, "rust-template/template/src/template.rs.tmpl"), 29 | } 30 | // Make the build script executable 31 | err = GenerateFilesFromMap(output, source, files) 32 | if err != nil { 33 | return err 34 | } 35 | os.Chmod(path.Join(output, "build.sh"), os.FileMode(0755)) 36 | git, err := exec.LookPath("git") 37 | if err == nil { 38 | cmd := exec.Command(git, "rev-parse", "--is-inside-work-tree") 39 | stdout, _ := cmd.Output() 40 | if strings.Contains(string(stdout), "true") { 41 | exec.Command(git, "update-index", "--chmod=+x", "build.sh") 42 | } 43 | return nil 44 | } else { 45 | return err 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /internal/templates/rust.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "path" 7 | "strings" 8 | ) 9 | 10 | func RustGenerator(output string, source Source) error { 11 | err := GenerateCommon(output, source) 12 | if err != nil { 13 | return err 14 | } 15 | 16 | files := map[string]func() []byte{ 17 | "/src/lib.rs": templateFactory(box, "rust/src/lib.rs.tmpl"), 18 | "/Cargo.toml": templateFactory(box, "rust/Cargo.toml.tmpl"), 19 | } 20 | if len(source.TemplateName) == 0 { 21 | os.MkdirAll(output+"/.cargo", os.FileMode(0754)) 22 | files["/.cargo/config"] = templateFactory(box, "rust/.cargo/config.tmpl") 23 | files["/build.sh"] = templateFactory(box, "rust/build.sh.tmpl") 24 | files["/build.ps1"] = templateFactory(box, "rust/build.ps1.tmpl") 25 | } 26 | // Make the build script executable 27 | err = GenerateFilesFromMap(output, source, files) 28 | if err != nil { 29 | return err 30 | } 31 | os.Chmod(path.Join(output, "build.sh"), os.FileMode(0755)) 32 | git, err := exec.LookPath("git") 33 | if err == nil { 34 | cmd := exec.Command(git, "rev-parse", "--is-inside-work-tree") 35 | stdout, _ := cmd.Output() 36 | if strings.Contains(string(stdout), "true") { 37 | exec.Command(git, "update-index", "--chmod=+x", "build.sh") 38 | } 39 | return nil 40 | } else { 41 | return err 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /internal/templates/templates.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | "sync" 8 | "text/template" 9 | "unicode" 10 | 11 | "github.com/Aidoku/aidoku-cli/internal/common" 12 | rice "github.com/GeertJohan/go.rice" 13 | "github.com/fatih/color" 14 | "github.com/iancoleman/strcase" 15 | "golang.org/x/text/runes" 16 | "golang.org/x/text/transform" 17 | "golang.org/x/text/unicode/norm" 18 | ) 19 | 20 | type Source struct { 21 | Language, Name, Homepage, TemplateName string 22 | Nsfw int 23 | } 24 | 25 | var ( 26 | box = rice.MustFindBox("resources") 27 | ) 28 | 29 | func templateFactory(box *rice.Box, path string) func() []byte { 30 | return func() []byte { 31 | return box.MustBytes(path) 32 | } 33 | } 34 | 35 | type ToPascalCase struct { 36 | } 37 | 38 | func (t ToPascalCase) Transform(dst, src []byte, atEOF bool) (nDst int, nSrc int, err error) { 39 | result := []byte(strcase.ToCamel(string(src))) 40 | nDst = copy(dst, result) 41 | nSrc = len(src) 42 | if nDst < nSrc { 43 | err = transform.ErrShortDst 44 | } 45 | return 46 | } 47 | 48 | func (t ToPascalCase) Reset() { 49 | 50 | } 51 | 52 | func slugifyFactory(whitespaceReplacer string, t transform.Transformer) func(string) string { 53 | return func(val string) string { 54 | temp := strings.ReplaceAll(strings.TrimSpace(val), " ", whitespaceReplacer) 55 | ret, _, _ := transform.String(t, temp) 56 | return ret 57 | } 58 | } 59 | 60 | func GenerateFilesFromMap(output string, source Source, files map[string]func() []byte) error { 61 | funcMap := template.FuncMap{ 62 | "ToLower": strings.ToLower, 63 | "SlugifyRust": slugifyFactory("_", transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)), 64 | "SlugifyAs": slugifyFactory("-", transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)), 65 | "SlugifyClass": slugifyFactory(" ", transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC, ToPascalCase{})), 66 | } 67 | var wg sync.WaitGroup 68 | errc := make(chan error, 10) 69 | for key, value := range files { 70 | wg.Add(1) 71 | go func(key string, value func() []byte) { 72 | defer wg.Done() 73 | file, err := os.Create(output + key) 74 | if err != nil { 75 | color.Red("error: could not create %s: %s", key, err.Error()) 76 | errc <- err 77 | return 78 | } 79 | defer file.Close() 80 | if filepath.Ext(key) == "sh" { 81 | os.Chmod(output+key, os.FileMode(0755)) 82 | } 83 | 84 | fileTemplate := template.Must(template.New(key).Funcs(funcMap).Parse(string(value()))) 85 | err = fileTemplate.Execute(file, source) 86 | if err != nil { 87 | color.Red("error: could not generate %s from template: %s", key, err.Error()) 88 | errc <- err 89 | return 90 | } 91 | }(key, value) 92 | } 93 | go func() { 94 | wg.Wait() 95 | close(errc) 96 | }() 97 | 98 | for err := range errc { 99 | if err != nil { 100 | return err 101 | } 102 | } 103 | return nil 104 | } 105 | 106 | func GenerateCommon(output string, source Source) error { 107 | // Create source directory 108 | os.MkdirAll(output+"/src", os.FileMode(0754)) 109 | os.MkdirAll(output+"/res", os.FileMode(0754)) 110 | 111 | // generate placeholder Icon.png 112 | err := common.GeneratePng(output + "/res/Icon.png") 113 | if err != nil { 114 | color.Red("error: could not generate placeholder icon") 115 | return err 116 | } 117 | 118 | files := map[string]func() []byte{ 119 | "/res/source.json": templateFactory(box, "common/res/source.json.tmpl"), 120 | "/res/filters.json": templateFactory(box, "common/res/filters.json.tmpl"), 121 | "/res/settings.json": templateFactory(box, "common/res/settings.json.tmpl"), 122 | } 123 | return GenerateFilesFromMap(output, source, files) 124 | } 125 | -------------------------------------------------------------------------------- /scripts/completions: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | rm -rf completions 4 | mkdir completions 5 | for sh in bash zsh fish; do 6 | go run ./cmd/aidoku completion "$sh" > "completions/_$sh" 7 | done 8 | --------------------------------------------------------------------------------