From e715024cbd787187c6db8201ade3889b2d5197bd Mon Sep 17 00:00:00 2001 From: Craig Williams Date: Wed, 23 Sep 2026 14:32:50 +0100 Subject: [PATCH] Initial project --- .gitignore | 43 +++++++++ .golangci.json | 41 ++++++++ LICENSE | 165 ++++++++++++++++++++++++++++++++ README.md | 4 + cmd/root.go | 118 +++++++++++++++++++++++ go.mod | 26 +++++ go.sum | 40 ++++++++ internal/cli/config/command.go | 41 ++++++++ internal/cli/config/get.go | 83 ++++++++++++++++ internal/cli/config/init.go | 96 +++++++++++++++++++ internal/cli/config/set.go | 141 +++++++++++++++++++++++++++ internal/cli/config/show.go | 62 ++++++++++++ internal/cli/version/command.go | 54 +++++++++++ internal/cli/with_config.go | 43 +++++++++ internal/config/conf_logging.go | 108 +++++++++++++++++++++ internal/config/config.go | 114 ++++++++++++++++++++++ internal/config/debug.go | 60 ++++++++++++ internal/config/load.go | 96 +++++++++++++++++++ internal/config/print.go | 75 +++++++++++++++ internal/log/logging.go | 63 ++++++++++++ internal/version/version.go | 8 ++ main.go | 25 +++++ scripts/build.sh | 41 ++++++++ 23 files changed, 1547 insertions(+) create mode 100644 .gitignore create mode 100644 .golangci.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cmd/root.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/cli/config/command.go create mode 100644 internal/cli/config/get.go create mode 100644 internal/cli/config/init.go create mode 100644 internal/cli/config/set.go create mode 100644 internal/cli/config/show.go create mode 100644 internal/cli/version/command.go create mode 100644 internal/cli/with_config.go create mode 100644 internal/config/conf_logging.go create mode 100644 internal/config/config.go create mode 100644 internal/config/debug.go create mode 100644 internal/config/load.go create mode 100644 internal/config/print.go create mode 100644 internal/log/logging.go create mode 100644 internal/version/version.go create mode 100644 main.go create mode 100755 scripts/build.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c988110 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# ---> Go +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# config file(s) +config.yaml +config.json + +# Visual Studio Code +.vscode/* + +# Build directory +build/ + +# Release directory +release/ + +# Generated code directories +internal/config/util/ +!internal/config/util/util.go \ No newline at end of file diff --git a/.golangci.json b/.golangci.json new file mode 100644 index 0000000..6cb71a8 --- /dev/null +++ b/.golangci.json @@ -0,0 +1,41 @@ +{ + "version": "2", + "run": { + "timeout": "5m", + "tests": true + }, + "linters": { + "enable": [ + "cyclop", + "dogsled", + "errcheck", + "godox", + "govet", + "gosec", + "ineffassign", + "nilerr", + "nilnil", + "nolintlint", + "unused", + "whitespace" + ] + }, + "formatters": { + "enable": [ + "gci", + "gofmt", + "goimports", + "golines" + ], + "settings": { + "golines": { + "max-len": 100, + "tab-len": 2 + } + } + }, + "issues": { + "max-same-issues": 0, + "max-issues-per-linter": 0 + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..65c5ca8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2e3c28d --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# JobSearch + +JobSearch is a service for scraping for contract opportunities. It is primarily +a re-write of the existing nodeJS JobSearch codebase. diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..8153ad3 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,118 @@ +/* +Copyright © 2026 Craig Williams + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program. If not, see . +*/ +package cmd + +import ( + "log/slog" + "os" + "strings" + + "git.avsdev.uk/Go/job-search/internal/cli" + cmdConfig "git.avsdev.uk/Go/job-search/internal/cli/config" + cmdVersion "git.avsdev.uk/Go/job-search/internal/cli/version" + "git.avsdev.uk/Go/job-search/internal/config" + "git.avsdev.uk/Go/job-search/internal/log" + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "job-search", + Short: "JobSearch - AVSDev Future Opportunities!", + Long: `JobSearch is a scraping tool for searching for possible contract +opportunities. + `, + CompletionOptions: cobra.CompletionOptions{ + HiddenDefaultCmd: true, + }, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // Check if the command is expecting to use terminal for messages + if val, ok := cmd.Annotations["terminal_log"]; ok && val == "1" { + log.LogToTerminal() + } + + // Check for debug level via args + dbgCount, _ := cmd.Flags().GetCount("debug") + conf := config.JobSearchConfig{ + Logging: config.JobSearchConfigLogging{}, + } + if dbgCount > 1 { + conf.Logging.Level = "debug" + } else if dbgCount > 0 { + conf.Logging.Level = "info" + } + if conf.Logging.LogLevel() <= slog.LevelDebug { + log.EnableDebug() + } else if conf.Logging.LogLevel() <= slog.LevelInfo { + log.EnableInfo() + } + + // With logging set up, these will be visible if trace is active + slog.Debug("root.PersistentPreRun") + slog.Debug( + "command: \"" + + strings.Join(strings.Split(cmd.CommandPath(), " ")[1:], " ") + + "\"", + ) + + // Always load config unless command indicates not to + if val, ok := cmd.Annotations["no_config"]; !ok || val == "0" { + // Load config - from specified file if provided + cfgFile, _ := cmd.Flags().GetString("config") + if err := config.Init(cfgFile, &conf); err != nil { + return err + } + + // Attach config to command context + ctx := cli.WithConfig(cmd.Context(), conf) + cmd.SetContext(ctx) + + // Change logging level based off newly loaded config + if conf.Logging.LogLevel() <= slog.LevelDebug { + log.EnableDebug() + } + } + + return nil + }, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + slog.Debug("root.PersistentPostRun") + }, +} + +// Execute, called by main.main() handles all child commands and appropriate +// flags. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + cobra.EnableTraverseRunHooks = true + + log.Init() + + pFlags := rootCmd.PersistentFlags() + pFlags.StringP( + "config", "c", "", + "config file (default: "+config.EtcConfigPath+"/config.yaml)") + pFlags.CountP("debug", "d", "enable debug output (repeat for trace)") + + rootCmd.AddCommand(cmdVersion.NewCommand()) + rootCmd.AddCommand(cmdConfig.NewCommand()) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..13061ef --- /dev/null +++ b/go.mod @@ -0,0 +1,26 @@ +module git.avsdev.uk/Go/job-search + +go 1.26.8 + +require ( + github.com/joho/godotenv v1.5.1 + github.com/lmittmann/tint v1.2.0 + github.com/spf13/cobra v1.10.2 + github.com/spf13/viper v1.21.0 + go.yaml.in/yaml/v3 v3.0.5 +) + +require ( + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.28.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9cc21fd --- /dev/null +++ b/go.sum @@ -0,0 +1,40 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/lmittmann/tint v1.2.0 h1:AogHRHy8HUJUnNJBHJlYa+fR4YY8mko2cnCp67xn9JY= +github.com/lmittmann/tint v1.2.0/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/cli/config/command.go b/internal/cli/config/command.go new file mode 100644 index 0000000..4462f19 --- /dev/null +++ b/internal/cli/config/command.go @@ -0,0 +1,41 @@ +/* +Copyright © 2026 Craig Williams + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program. If not, see . +*/ +package config + +import ( + "log/slog" + + "github.com/spf13/cobra" +) + +func NewCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "config", + Short: "Configuration management", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + slog.Debug("config.PersistentPreRun") + }, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + slog.Debug("config.PersistentPostRun") + }, + } + cmd.AddCommand(commandShow()) + cmd.AddCommand(commandGet()) + cmd.AddCommand(commandSet()) + cmd.AddCommand(commandInit()) + return cmd +} diff --git a/internal/cli/config/get.go b/internal/cli/config/get.go new file mode 100644 index 0000000..3eff309 --- /dev/null +++ b/internal/cli/config/get.go @@ -0,0 +1,83 @@ +/* +Copyright © 2026 Craig Williams + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program. If not, see . +*/ +package config + +import ( + "errors" + "fmt" + "log/slog" + + "git.avsdev.uk/Go/job-search/internal/cli" + "github.com/spf13/cobra" +) + +func commandGet() *cobra.Command { + cmd := &cobra.Command{ + Use: "get [config_key]", + Args: cobra.ExactArgs(1), + // Args: func(cmd *cobra.Command, args []string) error { + // // Optionally run one of the validators provided by cobra + // if err := cobra.ExactArgs(1)(cmd, args); err != nil { + // return err + // } + + // conf, ok := cli.ConfigFromContext(cmd.Context()) + // if !ok { + // return errors.New("no configuration loaded") + // } + + // flatConf := config.FlattenConfig(conf) + // if !util.HasKey(flatConf, args[0]) { + // return fmt.Errorf("invalid config key specified: %s", args[0]) + // } + + // return nil + // }, + Short: "Get a value from configuration", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + slog.Debug("config.get.PersistentPreRun") + }, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + slog.Debug("config.get.PersistentPostRun") + }, + RunE: func(cmd *cobra.Command, args []string) error { + slog.Debug("config.get.Run") + + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + + conf, ok := cli.ConfigFromContext(cmd.Context()) + if !ok { + return errors.New("no configuration loaded") + } + + v, err := conf.ValueOf(args[0]) + if err != nil { + return err + } + t, _ := conf.TypeOf(args[0]) + fmt.Println(v, "<"+t.String()+">") + + return nil + }, + Annotations: map[string]string{ + "terminal_log": "1", + }, + } + return cmd +} diff --git a/internal/cli/config/init.go b/internal/cli/config/init.go new file mode 100644 index 0000000..7e6b543 --- /dev/null +++ b/internal/cli/config/init.go @@ -0,0 +1,96 @@ +/* +Copyright © 2026 Craig Williams + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program. If not, see . +*/ +package config + +import ( + "errors" + "fmt" + "log/slog" + "os" + + "git.avsdev.uk/Go/job-search/internal/config" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func commandInit() *cobra.Command { + cmd := &cobra.Command{ + Use: "init [config_path]", + Args: cobra.MatchAll(cobra.MinimumNArgs(0), cobra.MaximumNArgs(1)), + Short: "Create an empty configuration file", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + slog.Debug("config.init.PersistentPreRun") + }, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + slog.Debug("config.init.PersistentPostRun") + }, + RunE: func(cmd *cobra.Command, args []string) error { + slog.Debug("config.init.Run") + + if err := cobra.MinimumNArgs(0)(cmd, args); err != nil { + return err + } + if err := cobra.MaximumNArgs(1)(cmd, args); err != nil { + return err + } + + cfgFile, _ := cmd.Flags().GetString("config") + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } else { + // Search config in /etc or local directory. + viper.AddConfigPath(config.EtcConfigPath) + viper.AddConfigPath(".") + viper.SetConfigName("config") + } + + err := viper.ReadInConfig() + if err == nil { + return errors.New("configuration file already exists") + } + + _, ok := err.(viper.ConfigFileNotFoundError) + if !errors.Is(err, os.ErrNotExist) && !ok { + // Config file was found but another error was produced + return err + } + + // Load default configuration + config.LoadDefaultConfig() + + if viper.ConfigFileUsed() == "" { + viper.SetConfigFile(config.EtcConfigPath + "/config.yaml") + } + + fmt.Print( + "writing default configuration to file: " + + viper.ConfigFileUsed() + + "\n") + + if err := viper.WriteConfig(); err != nil { + return err + } + return nil + }, + Annotations: map[string]string{ + "no_config": "1", + "terminal_log": "1", + }, + } + return cmd +} diff --git a/internal/cli/config/set.go b/internal/cli/config/set.go new file mode 100644 index 0000000..195e5eb --- /dev/null +++ b/internal/cli/config/set.go @@ -0,0 +1,141 @@ +/* +Copyright © 2026 Craig Williams + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program. If not, see . +*/ +package config + +import ( + "errors" + "fmt" + "log/slog" + "reflect" + "strconv" + + "git.avsdev.uk/Go/job-search/internal/cli" + "git.avsdev.uk/Go/job-search/internal/config" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func setConfig(conf config.JobSearchConfig, key string, value string) error { + var confValue any = nil + var confType reflect.Kind + var err error + + confType, err = conf.TypeOf(key) + if err != nil { + return err + } + + switch confType { + case reflect.Bool: + confValue, err = strconv.ParseBool(value) + case reflect.String: + confValue = value + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + confValue, err = strconv.ParseInt(value, 10, 64) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + confValue, err = strconv.ParseUint(value, 10, 64) + case reflect.Float32, reflect.Float64: + confValue, err = strconv.ParseFloat(value, 64) + default: + err = errors.ErrUnsupported + } + if err != nil { + return fmt.Errorf("config value type not accepted, expected <%s>", confType) + } + + viper.Set(key, confValue) + if err := viper.WriteConfig(); err != nil { + return err + } + fmt.Print(key, " = ", confValue, " <", confType, ">\n") + + return nil +} +func unsetConfig(conf config.JobSearchConfig, key string) error { + if _, err := conf.TypeOf(key); err != nil { + return err + } + + v := viper.New() + v.SetConfigFile(viper.ConfigFileUsed()) + + keys := viper.AllKeys() + for i := range keys { + k := keys[i] + if k != key { + v.Set(k, viper.Get(k)) + } + } + + if err := v.WriteConfig(); err != nil { + return err + } + + return nil +} + +func commandSet() *cobra.Command { + cmd := &cobra.Command{ + Use: "set [config_key] [config_value]", + Args: cobra.MatchAll(cobra.MinimumNArgs(1), cobra.MaximumNArgs(2)), + Short: "Set a value to configuration", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + slog.Debug("config.set.PersistentPreRun") + }, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + slog.Debug("config.set.PersistentPostRun") + }, + RunE: func(cmd *cobra.Command, args []string) error { + slog.Debug("config.set.Run") + + if err := cobra.MinimumNArgs(1)(cmd, args); err != nil { + return err + } + if err := cobra.MaximumNArgs(2)(cmd, args); err != nil { + return err + } + + unset, unsetFlagErr := cmd.Flags().GetBool("unset") + if unsetFlagErr != nil { + return unsetFlagErr + } + if err := cobra.ExactArgs(2)(cmd, args); err != nil && !unset { + return err + } + + conf, ok := cli.ConfigFromContext(cmd.Context()) + if !ok { + return errors.New("no configuration loaded") + } + + if viper.ConfigFileUsed() == "" { + return errors.New("no configuration file exists") + } + + if unset { + return unsetConfig(conf, args[0]) + } else { + return setConfig(conf, args[0], args[1]) + } + }, + Annotations: map[string]string{ + "terminal_log": "1", + }, + } + cmd.Flags().BoolP("unset", "x", false, "clear configuration setting") + return cmd +} diff --git a/internal/cli/config/show.go b/internal/cli/config/show.go new file mode 100644 index 0000000..0b4c862 --- /dev/null +++ b/internal/cli/config/show.go @@ -0,0 +1,62 @@ +/* +Copyright © 2026 Craig Williams + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program. If not, see . +*/ +package config + +import ( + "errors" + "log/slog" + + "git.avsdev.uk/Go/job-search/internal/cli" + "github.com/spf13/cobra" +) + +func commandShow() *cobra.Command { + cmd := &cobra.Command{ + Use: "show", + Short: "Print config information", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + slog.Debug("config.show.PersistentPreRun") + }, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + slog.Debug("config.show.PersistentPostRun") + }, + RunE: func(cmd *cobra.Command, args []string) error { + slog.Debug("config.show.Run") + + conf, ok := cli.ConfigFromContext(cmd.Context()) + if !ok { + return errors.New("no configuration loaded") + } + + if yaml, _ := cmd.Flags().GetBool("yaml"); yaml { + conf.PrintYaml() + } else if json, _ := cmd.Flags().GetBool("json"); json { + conf.PrintJson() + } else { + conf.PrintTerminal() + } + + return nil + }, + Annotations: map[string]string{ + "terminal_log": "1", + }, + } + cmd.Flags().Bool("json", false, "format configuration as json") + cmd.Flags().Bool("yaml", false, "format configuration as yaml") + return cmd +} diff --git a/internal/cli/version/command.go b/internal/cli/version/command.go new file mode 100644 index 0000000..0366861 --- /dev/null +++ b/internal/cli/version/command.go @@ -0,0 +1,54 @@ +/* +Copyright © 2026 Craig Williams + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program. If not, see . +*/ +package version + +import ( + "fmt" + "log/slog" + + "git.avsdev.uk/Go/job-search/internal/version" + "github.com/spf13/cobra" +) + +func NewCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "version", + Short: "Print version information", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + slog.Debug("version.PersistentPreRun") + }, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + slog.Debug("version.PersistentPostRun") + }, + Run: func(cmd *cobra.Command, args []string) { + slog.Debug("version.Run") + + fmt.Printf("PBCNinja - %s\n", version.Version) + if version.BuildTime != "unknown" { + fmt.Printf("Built: %s\n", version.BuildTime) + } + if version.GitCommit != "unknown" { + fmt.Printf("Commit: %s\n", version.GitCommit) + } + }, + Annotations: map[string]string{ + "no_config": "1", + "terminal_log": "1", + }, + } + return cmd +} diff --git a/internal/cli/with_config.go b/internal/cli/with_config.go new file mode 100644 index 0000000..ee4c649 --- /dev/null +++ b/internal/cli/with_config.go @@ -0,0 +1,43 @@ +/* +Copyright © 2026 Craig Williams + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program. If not, see . +*/ +package cli + +import ( + "context" + + "git.avsdev.uk/Go/job-search/internal/config" +) + +// Define a private key type to avoid collisions +type configContextKey struct { + name string +} + +// Define the key +var configKey = &configContextKey{"config"} + +// Public accessor to store a value to ctx +func WithConfig(ctx context.Context, conf config.JobSearchConfig) context.Context { + // No allocation here since userIDKey is a pointer to a struct + return context.WithValue(ctx, configKey, conf) +} + +// Public accessor to fetch a value from ctx +func ConfigFromContext(ctx context.Context) (config.JobSearchConfig, bool) { + v, ok := ctx.Value(configKey).(config.JobSearchConfig) + return v, ok +} diff --git a/internal/config/conf_logging.go b/internal/config/conf_logging.go new file mode 100644 index 0000000..4b24622 --- /dev/null +++ b/internal/config/conf_logging.go @@ -0,0 +1,108 @@ +/* +Copyright © 2026 Craig Williams + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program. If not, see . +*/ +package config + +import ( + "errors" + "log/slog" + "reflect" +) + +type JobSearchConfigLogging struct { + Level string +} + +// Default configuration +func defaultConfigLogging() JobSearchConfigLogging { + return JobSearchConfigLogging{ + Level: "warn", + } +} + +// Converts configuration to a map +func (conf JobSearchConfigLogging) Map() map[string]any { + return map[string]any{ + "level": conf.Level, + } +} + +// Converts configuration to a flat dotted format +func (conf JobSearchConfigLogging) Flatten() map[string]any { + confMap := conf.Map() + retMap := map[string]any{} + for k, v := range confMap { + retMap["logging."+k] = v + } + return retMap +} + +// Returns the expected type of a configuration key value +func (c JobSearchConfigLogging) TypeOf(key string) (reflect.Kind, error) { + switch key { + case "level": + return reflect.String, nil + default: + return reflect.Invalid, errors.New("unknown configuration key") + } +} + +// Returns the value of a configuration key +func (c JobSearchConfigLogging) ValueOf(key string) (any, error) { + switch key { + case "level": + return c.Level, nil + default: + return "", errors.New("unknown configuration key") + } +} + +// Sets the value of a configuration key +func (c *JobSearchConfigLogging) SetValue(key string, value any) error { + switch key { + case "level": + newLevel := value.(string) + switch newLevel { + case "debug", "info", "warn", "error": + c.Level = value.(string) + default: + return errors.New( + "invalid log level. Must be one of: debug, info, warn, error") + } + default: + return errors.New("unknown configuration key") + } + return nil +} + +func ConfigLevelToLogLevel(level string) slog.Level { + switch level { + case "debug": + return slog.LevelDebug + case "info": + return slog.LevelInfo + case "warn": + return slog.LevelWarn + case "error": + return slog.LevelError + default: + return 99 + } +} + +func (c *JobSearchConfigLogging) LogLevel() slog.Level { + return ConfigLevelToLogLevel(c.Level) +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..e7c6ecb --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,114 @@ +/* +Copyright © 2026 Craig Williams + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program. If not, see . +*/ +package config + +import ( + "errors" + "maps" + "reflect" + "strings" + + "github.com/spf13/viper" +) + +const EnvPrefix string = "JOBSEARCH" +const EtcConfigPath string = "/etc/job-search" + +type JobSearchConfig struct { + Logging JobSearchConfigLogging +} + +// Default configuration +func LoadDefaultConfig() { + conf := JobSearchConfig{ + Logging: defaultConfigLogging(), + } + LoadToViper(conf) +} + +// Converts configuration to a map +func (conf JobSearchConfig) Map() map[string]any { + confMap := map[string]any{} + confMap["logging"] = conf.Logging.Map() + return confMap +} + +// Converts configuration to a flat dotted format +func (conf JobSearchConfig) Flatten() map[string]any { + confFlat := map[string]any{} + maps.Copy(confFlat, conf.Logging.Flatten()) + return confFlat +} + +// Returns the expected type of a configuration key value +func (c JobSearchConfig) TypeOf(key string) (reflect.Kind, error) { + keyParts := strings.Split(key, ".") + switch keyParts[0] { + case "logging": + return c.Logging.TypeOf(strings.Join(keyParts[1:], ".")) + default: + return reflect.Invalid, errors.New("unknown configuration key") + } +} + +// Returns the expected type of a configuration value +func (c JobSearchConfig) ValueOf(key string) (any, error) { + keyParts := strings.Split(key, ".") + switch keyParts[0] { + case "logging": + return c.Logging.ValueOf(strings.Join(keyParts[1:], ".")) + default: + return nil, errors.New("unknown configuration key") + } +} + +// Returns the expected type of a configuration value +func (c *JobSearchConfig) SetValue(key string, value any) error { + keyParts := strings.Split(key, ".") + switch keyParts[0] { + case "logging": + return c.Logging.SetValue(strings.Join(keyParts[1:], "."), value) + default: + return errors.New("unknown configuration key") + } +} + +func LoadFromViper(conf *JobSearchConfig) { + keys := viper.AllKeys() + for idx := range keys { + key := keys[idx] + keyType, _ := conf.TypeOf(key) + if keyType == reflect.Invalid { + continue + } + switch keyType { + case reflect.String: + conf.SetValue(key, viper.GetString(key)) + case reflect.Int: + conf.SetValue(key, viper.GetInt(key)) + case reflect.Bool: + conf.SetValue(key, viper.GetBool(key)) + } + } +} + +func LoadToViper(conf JobSearchConfig) { + flatMap := conf.Flatten() + for key := range flatMap { + viper.SetDefault(key, flatMap[key]) + } +} diff --git a/internal/config/debug.go b/internal/config/debug.go new file mode 100644 index 0000000..c7465dd --- /dev/null +++ b/internal/config/debug.go @@ -0,0 +1,60 @@ +/* +Copyright © 2026 Craig Williams + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program. If not, see . +*/ +package config + +import ( + "fmt" + "log/slog" + "os" + "reflect" + "slices" + "strings" + + "github.com/spf13/viper" +) + +// Debug: Prints detected environment variables +func printEnv() { + slog.Debug("Environment:") + env := os.Environ() + slices.Sort(env) + for _, e := range env { + pair := strings.SplitN(e, "=", 2) + if strings.HasPrefix(pair[0], EnvPrefix) { + slog.Debug("\t" + pair[0] + "=" + pair[1]) + } + } +} + +// Debug: Prints loaded configuration +func printLoadedConfig() { + slog.Debug("Loaded config:") + keys := viper.AllKeys() + for i := range keys { + k := keys[i] + v := viper.Get(k) + t := reflect.TypeOf(v) + slog.Debug( + "\t" + k + " = " + fmt.Sprintf("%#v", v) + " <" + t.Kind().String() + ">") + } +} + +// Debug: Prints parsed configuration +func printConfig(conf JobSearchConfig) { + slog.Debug("Config:") + conf.PrintLog(slog.LevelDebug) +} diff --git a/internal/config/load.go b/internal/config/load.go new file mode 100644 index 0000000..4595f35 --- /dev/null +++ b/internal/config/load.go @@ -0,0 +1,96 @@ +/* +Copyright © 2026 Craig Williams + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program. If not, see . +*/ +package config + +import ( + "fmt" + "log/slog" + + "github.com/joho/godotenv" + "github.com/spf13/viper" +) + +// Reads in dotenv file for configuration +func loadEnv() error { + if err := godotenv.Load(); err != nil { + return fmt.Errorf("invalid .env file: %w", err) + } + return nil +} + +// Reads in config file and ENV variables if set to form a configuration. +func loadConfig(cfgFile string) error { + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } else { + // Search config in /etc or local directory. + viper.AddConfigPath(EtcConfigPath) + viper.AddConfigPath(".") + + viper.SetConfigName("config") + } + + // Load default configuration + LoadDefaultConfig() + + viper.SetEnvPrefix(EnvPrefix) + viper.AutomaticEnv() // read in environment variables that match + + err := viper.ReadInConfig() + if err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + // Indicates an explicitly set config file is not found or + // that no config file was found in any search path + slog.Warn("No config file found!") + } else { + // Config file was found but another error was produced + return err + } + } else { + // Config file found and successfully parsed + slog.Info("Using config file: " + viper.ConfigFileUsed()) + } + return nil +} + +// Initilises environment file configuration and general app configuration +func Init(cfgFile string, conf *JobSearchConfig) error { + if err := loadEnv(); err != nil { + return err + } + if conf.Logging.LogLevel() <= slog.LevelDebug { + printEnv() + } + + if err := loadConfig(cfgFile); err != nil { + return err + } + + oldLogLevel := conf.Logging.Level + LoadFromViper(conf) + if ConfigLevelToLogLevel(oldLogLevel) < conf.Logging.LogLevel() { + conf.Logging.Level = oldLogLevel + } + + if conf.Logging.LogLevel() <= slog.LevelDebug { + printLoadedConfig() + printConfig(*conf) + } + + return nil +} diff --git a/internal/config/print.go b/internal/config/print.go new file mode 100644 index 0000000..158a9a6 --- /dev/null +++ b/internal/config/print.go @@ -0,0 +1,75 @@ +/* +Copyright © 2026 Craig Williams + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program. If not, see . +*/ +package config + +import ( + "encoding/json" + "fmt" + "log/slog" + + "go.yaml.in/yaml/v3" +) + +// Prints configuration to stdout in flat dotted format +func (conf JobSearchConfig) PrintTerminal() { + for k, v := range conf.Flatten() { + typ, _ := conf.TypeOf(k) + fmt.Print(k, " = ", v, " <", typ, ">\n") + } +} + +// Prints configuration to stdout as JSON +func (conf JobSearchConfig) PrintJson() { + confMap := conf.Map() + jbytes, _ := json.MarshalIndent(confMap, "", " ") + fmt.Print(string(jbytes) + "\n") +} + +// Prints configuration to stdout as YAML +func (conf JobSearchConfig) PrintYaml() { + confMap := conf.Map() + jbytes, _ := yaml.Marshal(confMap) + fmt.Print(string(jbytes) + "\n") +} + +// Prints configuration to logrus log at debug, info (default) or error level +func (conf JobSearchConfig) PrintLog(atLevel slog.Level) { + var logFn func(args ...interface{}) + + switch atLevel { + case slog.LevelDebug: + logFn = func(args ...interface{}) { + slog.Debug(fmt.Sprint(args...)) + } + case slog.LevelError: + logFn = func(args ...interface{}) { + slog.Error(fmt.Sprint(args...)) + } + case slog.LevelWarn: + logFn = func(args ...interface{}) { + slog.Warn(fmt.Sprint(args...)) + } + default: + logFn = func(args ...interface{}) { + slog.Info(fmt.Sprint(args...)) + } + } + + for k, v := range conf.Flatten() { + logFn("\t", k, " = ", v) + } +} diff --git a/internal/log/logging.go b/internal/log/logging.go new file mode 100644 index 0000000..7acbf39 --- /dev/null +++ b/internal/log/logging.go @@ -0,0 +1,63 @@ +/* +Copyright © 2026 Craig Williams + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program. If not, see . +*/ +package log + +import ( + "log/slog" + "os" + + "github.com/lmittmann/tint" +) + +var logLevel slog.LevelVar + +// Set up the logger using defaults +func Init() { + logLevel.Set(slog.LevelWarn) + opts := &slog.HandlerOptions{ + Level: &logLevel, + } + handler := slog.NewTextHandler(os.Stdout, opts) + logger := slog.New(handler) + slog.SetDefault(logger) +} + +// Enable info level logging +func EnableInfo() { + if logLevel.Level() > slog.LevelInfo { + logLevel.Set(slog.LevelInfo) + slog.Info("Info logging enabled") + } +} + +// Enable debug level logging +func EnableDebug() { + if logLevel.Level() > slog.LevelDebug { + logLevel.Set(slog.LevelDebug) + slog.Debug("Debug logging enabled") + } +} + +func LogToTerminal() { + opts := &tint.Options{ + Level: &logLevel, + TimeFormat: "*", + } + handler := tint.NewTextHandler(os.Stdout, opts) + logger := slog.New(handler) + slog.SetDefault(logger) +} diff --git a/internal/version/version.go b/internal/version/version.go new file mode 100644 index 0000000..c0180e9 --- /dev/null +++ b/internal/version/version.go @@ -0,0 +1,8 @@ +package version + +// Version information (set at build time with -ldflags) +var ( + Version = "dev" + BuildTime = "unknown" + GitCommit = "unknown" +) diff --git a/main.go b/main.go new file mode 100644 index 0000000..323e8c7 --- /dev/null +++ b/main.go @@ -0,0 +1,25 @@ +/* +Copyright © 2026 Craig Williams + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program. If not, see . +*/ +package main + +import ( + "git.avsdev.uk/Go/job-search/cmd" +) + +func main() { + cmd.Execute() +} diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..2f54329 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +# Build script for JobSearch release + +set -euo pipefail + +BUILD_DIR="build" +RELEASE_DIR="release" + +DO_RELEASE=1 + +BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S') +GIT_COMMIT= +VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo 'unknown') +VERSION_TAG=$(git describe --tags 2>/dev/null || echo 'unknown') + +if [ "$VERSION" != "$VERSION_TAG" ]; then + echo "Development build detected! (not installing to release)" + GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown') + VERSION="${VERSION}.${GIT_COMMIT}" + DO_RELEASE=0 +fi + +LD_FLAGS=${LD_FLAGS:-""} +LD_FLAGS="$LD_FLAGS -X git.avsdev.uk/Go/JobSearch/internal/version.Version=${VERSION}" +LD_FLAGS="$LD_FLAGS -X git.avsdev.uk/Go/JobSearch/internal/version.BuildTime=${BUILD_TIME}" +if [ "$GIT_COMMIT" != "" ]; then + LD_FLAGS="$LD_FLAGS -X git.avsdev.uk/Go/JobSearch/internal/version.GitCommit=${GIT_COMMIT}" +fi + +# Build binary with version info +go build \ + -ldflags="$LD_FLAGS" \ + -trimpath \ + -o $BUILD_DIR/job-search-$VERSION \ + ./ + +# If proper release, copy to output directory for packaging +if [[ $DO_RELEASE -eq 1 ]]; then + install -DT $BUILD_DIR/job-search-$VERSION $RELEASE_DIR/job-search +fi \ No newline at end of file