From 6b1f48b76e698cdd654c50f61d3a214501efdc16 Mon Sep 17 00:00:00 2001 From: devhindo Date: Fri, 6 Feb 2026 17:27:07 +0200 Subject: [PATCH 1/3] feat(x): Use Cobra CLI package to Implement CLI commands for authentication, tweeting, and versioning; refactor code structure and update dependencies Signed-off-by: devhindo --- src/cli/auth/oauth.go | 10 +++++----- src/cli/auth/user.go | 4 ++-- src/cli/cmd/auth.go | 34 ++++++++++++++++++++++++++++++++++ src/cli/cmd/future.go | 22 ++++++++++++++++++++++ src/cli/cmd/root.go | 29 +++++++++++++++++++++++++++++ src/cli/cmd/tweet.go | 20 ++++++++++++++++++++ src/cli/cmd/version.go | 19 +++++++++++++++++++ src/cli/go.mod | 6 ++++++ src/cli/go.sum | 11 +++++++++++ src/cli/main.go | 19 +++++++++++++++++-- 10 files changed, 165 insertions(+), 9 deletions(-) create mode 100644 src/cli/cmd/auth.go create mode 100644 src/cli/cmd/future.go create mode 100644 src/cli/cmd/root.go create mode 100644 src/cli/cmd/tweet.go create mode 100644 src/cli/cmd/version.go diff --git a/src/cli/auth/oauth.go b/src/cli/auth/oauth.go index 6826e88..ed76b41 100644 --- a/src/cli/auth/oauth.go +++ b/src/cli/auth/oauth.go @@ -3,13 +3,13 @@ package auth import ( "crypto/rand" "crypto/sha256" - "math/big" "encoding/base64" + "math/big" ) /* * code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) -*/ + */ func (u *User) generate_code_challenge() { // Base64-URL-encoded string of the SHA256 hash of the code verifier @@ -45,7 +45,7 @@ func (u *User) generate_code_verifier() { } b[i] = chars[n.Int64()] } - + u.Code_verifier = string(b) } @@ -56,7 +56,7 @@ func (u *User) generate_state(stateLength int) { panic(err) } - state := base64.URLEncoding.EncodeToString(b) + state := base64.RawURLEncoding.EncodeToString(b) u.State = state -} \ No newline at end of file +} diff --git a/src/cli/auth/user.go b/src/cli/auth/user.go index 820d14b..6ea3953 100644 --- a/src/cli/auth/user.go +++ b/src/cli/auth/user.go @@ -3,8 +3,8 @@ package auth import ( "fmt" - "github.com/devhindo/x/src/cli/utils" "github.com/devhindo/x/src/cli/lock" + "github.com/devhindo/x/src/cli/utils" ) type User struct { @@ -32,7 +32,7 @@ func (u *User) add_user_to_db() { if status != 200 { fmt.Println("error adding user") } else { - + err := lock.WriteLicenseKeyToFile(u.License) if err != nil { fmt.Println("coudln't write license key to file") diff --git a/src/cli/cmd/auth.go b/src/cli/cmd/auth.go new file mode 100644 index 0000000..0eeb1e5 --- /dev/null +++ b/src/cli/cmd/auth.go @@ -0,0 +1,34 @@ +package cmd + +import ( + "github.com/devhindo/x/src/cli/auth" + "github.com/devhindo/x/src/cli/clear" + "github.com/spf13/cobra" +) + +var authCmd = &cobra.Command{ + Use: "auth", + Short: "Authenticate with X", + Run: func(cmd *cobra.Command, args []string) { + verify, _ := cmd.Flags().GetBool("verify") + clearFlag, _ := cmd.Flags().GetBool("clear") + url, _ := cmd.Flags().GetBool("url") + + if verify { + auth.Verify() + } else if clearFlag { + clear.StartOver() + } else if url { + auth.Get_url_db() + } else { + auth.Auth() + } + }, +} + +func init() { + rootCmd.AddCommand(authCmd) + authCmd.Flags().BoolP("verify", "v", false, "Verify authentication") + authCmd.Flags().BoolP("clear", "c", false, "Clear authentication") + authCmd.Flags().Bool("url", false, "Get authorization URL") +} diff --git a/src/cli/cmd/future.go b/src/cli/cmd/future.go new file mode 100644 index 0000000..aec9d17 --- /dev/null +++ b/src/cli/cmd/future.go @@ -0,0 +1,22 @@ +package cmd + +import ( + "github.com/devhindo/x/src/cli/tweet" + "github.com/spf13/cobra" +) + +var futureCmd = &cobra.Command{ + Use: "future [message] [time]", + Short: "Post a future tweet", + Aliases: []string{"f"}, + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + // Construct fake os.Args for compatibility + fakeArgs := []string{"x", "f", args[0], args[1]} + tweet.PostFutureTweet(fakeArgs) + }, +} + +func init() { + rootCmd.AddCommand(futureCmd) +} diff --git a/src/cli/cmd/root.go b/src/cli/cmd/root.go new file mode 100644 index 0000000..0ccf0b0 --- /dev/null +++ b/src/cli/cmd/root.go @@ -0,0 +1,29 @@ +package cmd + +import ( + "os" + + "github.com/devhindo/x/src/cli/tweet" + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "x", + Short: "x CLI - Post tweets from your terminal", + Long: `x is a CLI tool that allows you to post tweets to X (formerly Twitter) directly from your terminal.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) > 0 { + // Join args just in case, but usually it's one arg "msg" + // The original code uses os.Args[1] or args[0] + tweet.POST_tweet(args[0]) + } else { + cmd.Help() + } + }, +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} diff --git a/src/cli/cmd/tweet.go b/src/cli/cmd/tweet.go new file mode 100644 index 0000000..eb37365 --- /dev/null +++ b/src/cli/cmd/tweet.go @@ -0,0 +1,20 @@ +package cmd + +import ( + "github.com/devhindo/x/src/cli/tweet" + "github.com/spf13/cobra" +) + +var tweetCmd = &cobra.Command{ + Use: "tweet [message]", + Short: "Post a tweet", + Aliases: []string{"t"}, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + tweet.POST_tweet(args[0]) + }, +} + +func init() { + rootCmd.AddCommand(tweetCmd) +} diff --git a/src/cli/cmd/version.go b/src/cli/cmd/version.go new file mode 100644 index 0000000..e81c7d9 --- /dev/null +++ b/src/cli/cmd/version.go @@ -0,0 +1,19 @@ +package cmd + +import ( + "github.com/devhindo/x/src/cli/x" + "github.com/spf13/cobra" +) + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Print version", + Aliases: []string{"v"}, + Run: func(cmd *cobra.Command, args []string) { + x.Version() + }, +} + +func init() { + rootCmd.AddCommand(versionCmd) +} diff --git a/src/cli/go.mod b/src/cli/go.mod index a9028b8..957e3a3 100644 --- a/src/cli/go.mod +++ b/src/cli/go.mod @@ -3,3 +3,9 @@ module github.com/devhindo/x/src/cli go 1.22.2 require github.com/google/uuid v1.3.1 + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.10 // indirect +) diff --git a/src/cli/go.sum b/src/cli/go.sum index a43f94d..d5ced09 100644 --- a/src/cli/go.sum +++ b/src/cli/go.sum @@ -1,2 +1,13 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/src/cli/main.go b/src/cli/main.go index c70a161..5c83ff6 100644 --- a/src/cli/main.go +++ b/src/cli/main.go @@ -1,8 +1,23 @@ package main -import "github.com/devhindo/x/src/cli/x" +import ( + "os" + "github.com/devhindo/x/src/cli/cmd" +) func main() { - x.Run() + // Support legacy commands starting with dash by rewriting them to their aliased subcommands + if len(os.Args) > 1 { + switch os.Args[1] { + case "-t": + os.Args[1] = "t" + case "-f": + os.Args[1] = "f" + case "-v": + os.Args[1] = "v" + } + } + + cmd.Execute() } From 35a78bd21005c26ba14e1ad34f194ebc1e3fcb4a Mon Sep 17 00:00:00 2001 From: devhindo Date: Fri, 6 Feb 2026 17:27:37 +0200 Subject: [PATCH 2/3] docs: Update README.md with detailed command structure and usage examples Signed-off-by: devhindo --- README.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/README.md b/README.md index 48b2b9b..bd73a41 100644 --- a/README.md +++ b/README.md @@ -128,3 +128,54 @@ Note: running `x auth -v` windows might flag the tool as a threat this is becaus ## Docs [Docs](https://deepwiki.com/devhindo/x) + +## Detailed Documentation + +The CLI has been updated to use a more robust command structure. + +### Global Options +- `-h, --help`: Help for any command + +### Commands + +#### `x [message]` +Post a tweet directly. +```bash +x "Hello World" +``` + +#### `tweet` (alias: `t`) +Post a tweet. +```bash +x tweet "Hello World" +x t "Hello World" +``` + +#### `auth` +Manage authentication. +- `x auth`: Start authentication flow +- `x auth --verify` (or `-v`): Verify authentication status +- `x auth --clear` (or `-c`): Clear stored credentials +- `x auth --url`: Display the authorization URL + +#### `future` (alias: `f`) +Schedule a tweet for later. +```bash +x future "Tweet later" 2h30m +x f "Tweet later" 5h +``` +Arguments: +1. Message: The tweet content +2. Duration: Time to wait (e.g., "1h", "30m", "1h30m") + +#### `version` (alias: `v`) +Print the CLI version. +```bash +x version +``` + +### Legacy Support +The following legacy flag styles are still supported for backward compatibility: +- `x -t "msg"` -> `x t "msg"` +- `x -f "msg" "time"` -> `x f "msg" "time"` +- `x -v` -> `x version` From cb4aef068a5360cf3736c54be97b601df29e317a Mon Sep 17 00:00:00 2001 From: devhindo Date: Fri, 6 Feb 2026 17:30:45 +0200 Subject: [PATCH 3/3] fix(tests): Clean up whitespace and correct base64 decoding in TestGenerateState Signed-off-by: devhindo --- src/cli/auth/oauth_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/cli/auth/oauth_test.go b/src/cli/auth/oauth_test.go index 5797dfb..a13d0cd 100644 --- a/src/cli/auth/oauth_test.go +++ b/src/cli/auth/oauth_test.go @@ -46,13 +46,13 @@ func TestGenerateCodeVerifier(t *testing.T) { func TestGenerateCodeChallenge(t *testing.T) { u := &User{} // Set a fixed verifier for reproducibility - u.Code_verifier = "hello_world_verifier_1234567890" - + u.Code_verifier = "hello_world_verifier_1234567890" + // sha256("hello_world_verifier_1234567890") // echo -n "hello_world_verifier_1234567890" | openssl sha256 -binary | base64 // hash = 84c3c33379967676e828114f851080d859e3557451965b1285268c375531d041 // Base64URL(hash) (without padding) - + u.generate_code_challenge() // Verify the result @@ -72,17 +72,17 @@ func TestGenerateState(t *testing.T) { // The implementation generates random bytes of 'length' then Base64 URL encodes them. // So the resulting string length will be roughly length * 4/3. - + if u.State == "" { t.Error("generate_state() produced empty state") } - + // Decode back to check byte length - decoded, err := base64.URLEncoding.DecodeString(u.State) + decoded, err := base64.RawURLEncoding.DecodeString(u.State) if err != nil { t.Errorf("generate_state() produced invalid base64: %v", err) } - + if len(decoded) != length { t.Errorf("generate_state() decoded length = %d, want %d", len(decoded), length) }