├── .gitignore ├── README.md ├── atlas.go ├── builds.go ├── credentials.go ├── git_info.go ├── index.html └── sites.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | atlas -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Atlas CLI 2 | 3 | A Go-based Atlas API client for building projects and publishing them as a web site. 4 | 5 | Here's the top-level help: 6 | 7 | ``` 8 | NAME: 9 | atlas - OºReilly Atlas command line tool 10 | 11 | USAGE: 12 | atlas [global options] command [command options] [arguments...] 13 | 14 | VERSION: 15 | 0.0.8-alpha 16 | 17 | COMMANDS: 18 | login Set your login/API credentials 19 | whoami Display your login/API credentials 20 | info Display info about your Atlas project based on the git config file 21 | build Build a project 22 | open Open a site 23 | publish Publish a site 24 | help, h Shows a list of commands or help for one command 25 | 26 | GLOBAL OPTIONS: 27 | --help, -h show help 28 | --version, -v print the version 29 | ``` 30 | 31 | ## Installation 32 | 33 | * Download the latest release 34 | * Put it on your path 35 | * Do `chmod + x` on it 36 | 37 | Someday I'll make a packager. 38 | 39 | 40 | ## Usage 41 | 42 | With this command, you can do things like this: 43 | 44 | 45 | * `atlas build -p odewahn/dds-field-guide --html` 46 | 47 | * `atlas publish -p odewahn/dds-field-guide --public` 48 | 49 | * `atlas open -p odewahn/dds-field-guide --public` 50 | 51 | If you omit the "-p" flag, the CLI will see if it can find a project name from the remotes defined in your git config. So, if you're in your project's home directory, you can just do this: 52 | 53 | * `atlas build --html` 54 | 55 | * `atlas publish --public` 56 | 57 | * `atlas open --public` 58 | 59 | 60 | ## Development 61 | 62 | To build, do this: 63 | 64 | ``` 65 | go build -o atlas *.go 66 | ``` 67 | 68 | -------------------------------------------------------------------------------- /atlas.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/codegangsta/cli" 6 | "os" 7 | ) 8 | 9 | // Declare package level vairables 10 | 11 | func main() { 12 | 13 | // Start the "real" program 14 | 15 | app := cli.NewApp() 16 | app.Name = "atlas" 17 | app.Usage = "OºReilly Atlas command line tool" 18 | app.Version = "0.1.0-alpha" 19 | app.Action = func(c *cli.Context) { 20 | fmt.Println("Nothing to do. Try `help` or `-h` to see what's possible.") 21 | } 22 | 23 | atlas_user := &Credentials{} 24 | atlas_user.Login() 25 | 26 | app.Commands = []cli.Command{ 27 | { 28 | Name: "login", 29 | Usage: "Set your login/API credentials", 30 | Action: func(c *cli.Context) { 31 | atlas_user.Query() 32 | atlas_user.Save() 33 | }, 34 | }, 35 | { 36 | Name: "whoami", 37 | Usage: "Display your login/API credentials", 38 | Action: func(c *cli.Context) { 39 | fmt.Printf("You are %s.\n", atlas_user.User) 40 | }, 41 | }, 42 | { 43 | Name: "info", 44 | Usage: "Display info about your Atlas project based on the git config file", 45 | Action: func(c *cli.Context) { 46 | fmt.Printf("Your project is %s\n", GetGitInfo()) 47 | }, 48 | }, 49 | { 50 | Name: "build", 51 | Usage: "Build a project", 52 | Flags: []cli.Flag{ 53 | cli.StringFlag{ 54 | Name: "project, p", 55 | Usage: "Project name", 56 | }, 57 | cli.BoolFlag{ 58 | Name: "pdf", 59 | Usage: " build a pdf", 60 | }, 61 | cli.BoolFlag{ 62 | Name: "html", 63 | Usage: " build html format", 64 | }, 65 | cli.BoolFlag{ 66 | Name: "epub", 67 | Usage: " build epub format", 68 | }, 69 | cli.BoolFlag{ 70 | Name: "mobi", 71 | Usage: " build mobi format", 72 | }, 73 | cli.StringFlag{ 74 | Name: "branch, b", 75 | Value: "master", 76 | Usage: "branch to build", 77 | }, 78 | }, 79 | Action: func(c *cli.Context) { 80 | args := &BuildArgs{} 81 | args.Parse(c) 82 | build := &Builds{} 83 | build.Build(*atlas_user, *args) 84 | }, 85 | }, 86 | { 87 | Name: "open", 88 | Usage: "Open a site", 89 | Flags: []cli.Flag{ 90 | cli.StringFlag{ 91 | Name: "project, p", 92 | Usage: "Project name", 93 | }, 94 | cli.BoolFlag{ 95 | Name: "public", 96 | Usage: "Make project public", 97 | }, 98 | }, 99 | Action: func(c *cli.Context){ 100 | s := &Sites{} 101 | s.Open(c) 102 | }, 103 | }, 104 | { 105 | Name: "publish", 106 | Usage: "Publish a site", 107 | Flags: []cli.Flag{ 108 | cli.StringFlag{ 109 | Name: "project, p", 110 | Usage: "Project name", 111 | }, 112 | cli.BoolFlag{ 113 | Name: "public", 114 | Usage: "Make project public", 115 | }, 116 | }, 117 | Action: func(c *cli.Context){ 118 | s := &Sites{} 119 | s.Publish(atlas_user , c) 120 | }, 121 | }, 122 | } 123 | 124 | app.Run(os.Args) 125 | 126 | } 127 | -------------------------------------------------------------------------------- /builds.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/codegangsta/cli" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "net/url" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | 16 | 17 | // Defines the payload of the build API 18 | type Builds struct { 19 | Build_url string `json:"build_url"` 20 | Message string `json:"message"` 21 | Status []struct { 22 | Format string `json:"format"` 23 | Status string `json:"status"` 24 | Download_url string `json:"download_url"` 25 | Message struct { 26 | Error string `json:"error"` 27 | } `json:"message"` 28 | } `json:"status"` 29 | } 30 | 31 | type ProjectBuilds []struct{ 32 | Id int `json:"id"` 33 | CreatedAt string `json:"created_at"` 34 | Status []struct { 35 | Format string `json:"format"` 36 | } `json:"status"` 37 | } 38 | 39 | 40 | // Hold the parameters for a build 41 | type BuildArgs struct { 42 | Project string 43 | Formats string 44 | Branch string 45 | } 46 | 47 | 48 | // This function returns the builds for the project 49 | // It uses the structure defined in builds.go 50 | 51 | func (builds *ProjectBuilds) Get(user *Credentials, project string) { 52 | 53 | qry := url.Values{ 54 | "project": {project}, 55 | "auth_token": {user.Key}, 56 | } 57 | 58 | resp, err := http.Get("https://atlas.oreilly.com/api/builds?" + qry.Encode() ) 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | defer resp.Body.Close() 63 | 64 | // Read the results from the build request 65 | body, err := ioutil.ReadAll(resp.Body) 66 | 67 | err = json.Unmarshal(body, &builds) 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | 72 | } 73 | 74 | 75 | func (args *BuildArgs) Parse(c *cli.Context) { 76 | 77 | // the project must be the first argument 78 | 79 | if len(c.String("project")) > 0 { 80 | args.Project = c.String("project") 81 | } else { 82 | args.Project = GetGitInfo() 83 | } 84 | 85 | 86 | //process the format flags they've requested 87 | formats := make([]string, 0) 88 | for _, f := range []string{"pdf", "html", "epub", "mobi"} { 89 | if c.Bool(f) { 90 | formats = append(formats, f) 91 | } 92 | } 93 | // if they haven't entered any formats, the build a PDF by default 94 | if len(formats) == 0 { 95 | formats = append(formats, "pdf") 96 | } 97 | args.Formats = strings.Join(formats, ",") 98 | 99 | // Now specifiy the branch 100 | args.Branch = "master" 101 | if len(c.String("branch")) > 0 { 102 | args.Branch = c.String("branch") 103 | } 104 | } 105 | 106 | // start a build for the project 107 | // Returns the URL to poll if successful 108 | func (builds *Builds) Start(c Credentials, a BuildArgs) { 109 | 110 | resp, err := http.PostForm("https://atlas.oreilly.com/api/builds", 111 | url.Values{ 112 | "project": {a.Project}, 113 | "auth_token": {c.Key}, 114 | "branch": {a.Branch}, 115 | "formats": {a.Formats}, 116 | }) 117 | if err != nil { 118 | log.Fatal(err) 119 | } 120 | defer resp.Body.Close() 121 | 122 | // Read the results from the build request 123 | body, err := ioutil.ReadAll(resp.Body) 124 | err = json.Unmarshal(body, &builds) 125 | 126 | } 127 | 128 | //start the build and then query it till it's done 129 | 130 | func (builds *Builds) Build(c Credentials, a BuildArgs) { 131 | 132 | fmt.Printf("Building %s",a.Project) 133 | 134 | builds.Start(c, a) 135 | 136 | build_url := fmt.Sprintf("https://atlas.oreilly.com%s?auth_token=%s", builds.Build_url, c.Key) 137 | var build_status Builds 138 | 139 | // now poll the build and see how it's doing 140 | for { 141 | resp, err := http.Get(build_url) 142 | if err != nil { 143 | log.Fatal(err) 144 | } 145 | defer resp.Body.Close() 146 | // Read the results from the build request 147 | body, err := ioutil.ReadAll(resp.Body) 148 | 149 | err = json.Unmarshal(body, &build_status) 150 | // now process the reuturn value. We should stop polling when all status values 151 | // are either "completed" or "failed" 152 | completed_count := 0 153 | for _, s := range build_status.Status { 154 | if (s.Status == "completed") || (s.Status == "failed") { 155 | completed_count += 1 156 | } 157 | } 158 | if completed_count == len(build_status.Status) { 159 | break 160 | } 161 | fmt.Print(".") 162 | time.Sleep(500 * time.Millisecond) 163 | } 164 | fmt.Println() 165 | 166 | // Now print the build info 167 | for _, s := range build_status.Status { 168 | if s.Status == "completed" { 169 | fmt.Printf("%s => %s\n", s.Format, s.Download_url) 170 | } else { 171 | fmt.Printf("%s => %s\n %s\n", s.Format, "Failed to build", s.Message.Error) 172 | } 173 | } 174 | 175 | } 176 | -------------------------------------------------------------------------------- /credentials.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/mitchellh/go-homedir" 8 | "io/ioutil" 9 | "os" 10 | ) 11 | 12 | var HOME_DIR, _ = homedir.Dir() //the users home directory where we'll put the credentials 13 | var CREDENTIAL_FILE = ".oreilly.json" //the name of the credentials file 14 | 15 | // Define API auth credentials structure 16 | type Credentials struct { 17 | User string 18 | Key string 19 | } 20 | 21 | // Fetch a value 22 | func prompt(s string) string { 23 | // Prompt the user for his or her credentials 24 | reader := bufio.NewReader(os.Stdin) 25 | fmt.Printf("%s: ", s) 26 | val, _ := reader.ReadString('\n') 27 | val = val[:len(val)-1] 28 | return val 29 | } 30 | 31 | // Get the users login credentials and save them to "~/.atlas.json" for the next time 32 | func (c *Credentials) Query() { 33 | c.User = prompt("Enter your Atlas user name") 34 | c.Key = prompt("Enter your Atlas key") 35 | } 36 | 37 | // Save the credentials to ~/.atlas.json 38 | func (c *Credentials) Save() { 39 | out, _ := json.Marshal(c) 40 | err := ioutil.WriteFile(HOME_DIR+"/" + CREDENTIAL_FILE, out, 0644) 41 | if err != nil { 42 | panic(err) 43 | } 44 | } 45 | 46 | // retreive the credentials from ~/.atlas.json 47 | func (c *Credentials) Load() error { 48 | credsJSON, err := ioutil.ReadFile(HOME_DIR + "/" + CREDENTIAL_FILE) 49 | if err == nil { 50 | err = json.Unmarshal(credsJSON, &c) 51 | if err != nil { return err } 52 | } else { 53 | return err 54 | } 55 | return err 56 | } 57 | 58 | //do the login 59 | func (c *Credentials) Login() { 60 | err := c.Load() 61 | if err != nil { 62 | c.Query() 63 | c.Save() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /git_info.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "log" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | 11 | func GetGitInfo() string { 12 | 13 | 14 | // Read in the gitconfig file 15 | file, err := os.Open("./.git/config") 16 | if err != nil { 17 | log.Println("Can't find the project. Use the -p option to specifiy the project you want to build.") 18 | log.Fatal(err) 19 | } 20 | defer file.Close() 21 | 22 | // find the first reference to an atlas repository 23 | remote := "" 24 | scanner := bufio.NewScanner(file) 25 | for scanner.Scan() { 26 | for _,s := range strings.Split(scanner.Text()," ") { 27 | if strings.Contains(s, "git@git.atlas.oreilly.com") { 28 | remote = s 29 | break 30 | } 31 | } 32 | } 33 | 34 | // Error our if we can't find anything 35 | if remote == "" { 36 | log.Fatal("Cannot find an atlas remote in the git config file. Use the -p option to suppy one directly.") 37 | } 38 | 39 | // Assuming we found the remote, parse out just the username and project, which will look like this: 40 | // git@git.atlas.oreilly.com:odewahn/ost-python.git 41 | 42 | repo := strings.Split(remote,":") 43 | if len(repo) !=2 { 44 | log.Fatal("Cannot find an atlas remote in the git config file. Use the -p option to suppy one directly.") 45 | } 46 | 47 | ret_val := repo[1] 48 | if strings.Contains(ret_val, ".git") { 49 | ret_val = ret_val[:len(ret_val)-4] 50 | } 51 | 52 | return ret_val 53 | 54 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Atlas CLI 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 |
29 |
30 |
31 | 32 |
33 |
34 |

Atlas Command Line Interface (CLI)

35 |
36 |
37 |
38 | 39 | 40 |
41 | 42 | 43 |
44 | 45 |

Atlas CLI allows you to build Atlas projects from your command line. Someday we'll make a real installer, but for now: 46 | 47 |

    48 |
  1. 49 | Download the binary for your OS (x86 architectures): 50 | 55 |
  2. 56 |
  3. Put the file somewhere on your main path
  4. 57 |
  5. For Mac or Linux, do "chmod +x"
  6. 58 |
  7. Report questions or issues
  8. 59 |
60 | Here's a link to all other architectures. 61 | 62 |
63 | 64 |
65 | 66 | 69 | 70 |
71 | 72 |
73 | 74 |
75 |
76 | 77 |
78 | 89 | 90 | 91 |
92 |
93 | 94 | 95 |
96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /sites.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/codegangsta/cli" 6 | "log" 7 | "github.com/toqueteos/webbrowser" 8 | "net/url" 9 | "io/ioutil" 10 | "net/http" 11 | "strconv" 12 | "encoding/json" 13 | "time" 14 | ) 15 | 16 | 17 | type Sites struct { 18 | Id int `json:"id"` 19 | URL string `json:"url"` 20 | State string `json:"state"` 21 | } 22 | 23 | 24 | //Find the first build with an HTML build 25 | func getBuildId(user *Credentials, project string) int { 26 | 27 | // Fetch the build data itself form the atlas api 28 | builds := &ProjectBuilds{} 29 | builds.Get(user, project) 30 | 31 | // loop though each build and build-> status till we find the first html build 32 | build_id := -1 33 | for _, b := range *builds { 34 | for _,s := range b.Status { 35 | //If we've found an HTML build, then set the build ID and break 36 | if s.Format == "html" { 37 | build_id = b.Id 38 | break 39 | } 40 | } 41 | //If we've found the HTML build, then break 42 | if build_id != -1 { 43 | break 44 | } 45 | } 46 | // if we don't find the build id then we need to error out 47 | if build_id == -1 { 48 | log.Fatal("Cannot find an html build for " + project) 49 | } 50 | return build_id 51 | } 52 | 53 | 54 | // Gets the status of the build with the given id 55 | func get_state(id int) string { 56 | 57 | url := fmt.Sprintf("http://web-publisher.atlas.oreilly.com/deploy/%d", id) 58 | resp, err := http.Get(url) 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | defer resp.Body.Close() 63 | // Read the results from the build request 64 | body, err := ioutil.ReadAll(resp.Body) 65 | 66 | var s Sites 67 | err = json.Unmarshal(body, &s) 68 | 69 | return s.State 70 | 71 | } 72 | 73 | 74 | func (s *Sites) Publish(user *Credentials, c *cli.Context) { 75 | 76 | project := "" 77 | if len(c.String("project")) > 0 { 78 | project = c.String("project") 79 | } else { 80 | project = GetGitInfo() 81 | } 82 | 83 | fmt.Printf("Publishing %s", project) 84 | 85 | // Get the visibility level 86 | bucket_type := "private" 87 | if c.Bool("public") { 88 | bucket_type = "public" 89 | } 90 | 91 | // get the build ID 92 | build_id := getBuildId(user,project) 93 | 94 | // Now hit the API endpoint to publish the build to sites 95 | resp, err := http.PostForm("http://web-publisher.atlas.oreilly.com/deploy", 96 | url.Values{ 97 | "build_id": {strconv.Itoa(build_id)}, 98 | "s3_path": {project}, 99 | "bucket_type": {bucket_type}, 100 | "token": {user.Key}, 101 | }) 102 | if err != nil { 103 | log.Fatal(err) 104 | } 105 | defer resp.Body.Close() 106 | 107 | // Read the results from the build request 108 | body, err := ioutil.ReadAll(resp.Body) 109 | 110 | var response Sites 111 | json.Unmarshal(body, &response) 112 | 113 | // Now poll until the build is complete 114 | for { 115 | fmt.Print(".") 116 | state := get_state(response.Id) 117 | if (state == "complete") { 118 | break 119 | } 120 | time.Sleep(500 * time.Millisecond) 121 | } 122 | 123 | s.Open(c) 124 | 125 | } 126 | 127 | func (s *Sites) Open(c *cli.Context) { 128 | 129 | project := "" 130 | if len(c.String("project")) > 0 { 131 | project = c.String("project") 132 | } else { 133 | project = GetGitInfo() 134 | } 135 | 136 | url := fmt.Sprintf("http://orm-static-site-proxy.herokuapp.com/%s/ch01.html", project) 137 | if c.Bool("public") { 138 | url = fmt.Sprintf("http://sites.oreilly.com/%s/ch01.html", project) 139 | } 140 | fmt.Printf("Opening %s\n", url) 141 | webbrowser.Open(url) 142 | } --------------------------------------------------------------------------------