Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fetch & download cardinal editor on world create #52

Merged
merged 11 commits into from
Apr 1, 2024
8 changes: 8 additions & 0 deletions cmd/world/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/rs/zerolog/log"

"pkg.world.dev/world-cli/cmd/world/root"
"pkg.world.dev/world-cli/common/globalconfig"
"pkg.world.dev/world-cli/telemetry"

_ "pkg.world.dev/world-cli/common/logger"
Expand Down Expand Up @@ -36,6 +37,13 @@ func main() {
// Set logger sentry hook
log.Logger = log.Logger.Hook(telemetry.SentryHook{})

// Set up config directory "~/.worldcli/"
err := globalconfig.SetupConfigDir()
if err != nil {
log.Err(err).Msg("could not setup config folder")
rmrt1n marked this conversation as resolved.
Show resolved Hide resolved
return
}

// Posthog Initialization
telemetry.PosthogInit(PosthogAPIKey)
defer telemetry.PosthogClose()
Expand Down
12 changes: 12 additions & 0 deletions cmd/world/root/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func NewWorldCreateModel(args []string) WorldCreateModel {
createSteps.Steps = []steps.Entry{
steps.NewStep("Set game shard name"),
steps.NewStep("Initialize game shard with starter-game-template"),
steps.NewStep("Set up Cardinal Editor"),
}

// Set the project text if it was passed in as an argument
Expand Down Expand Up @@ -122,6 +123,17 @@ func (m WorldCreateModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
teaCmd,
)
}
if msg.Index == 2 { //nolint:gomnd
err := teacmd.SetupCardinalEditor()
teaCmd := func() tea.Msg {
return teacmd.GitCloneFinishMsg{Err: err}
}

return m, tea.Sequence(
NewLogCmd(style.ChevronIcon.Render()+"Setting up Cardinal Editor"),
teaCmd,
)
}
return m, nil

case steps.SignalStepCompletedMsg:
Expand Down
28 changes: 28 additions & 0 deletions common/globalconfig/globalconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package globalconfig

import (
"os"
"path/filepath"
)

const (
configDir = ".worldcli"
)

func GetConfigDir() (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}

return filepath.Join(homeDir, configDir), nil
}

func SetupConfigDir() error {
fullConfigDir, err := GetConfigDir()
if err != nil {
return err
}

return os.MkdirAll(fullConfigDir, 0755)
}
271 changes: 271 additions & 0 deletions common/teacmd/editor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
package teacmd

import (
"archive/zip"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"

"github.com/google/uuid"

"pkg.world.dev/world-cli/common/globalconfig"
)

const (
latestReleaseURL = "https://api.github.com/repos/Argus-Labs/cardinal-editor/releases/latest"
httpTimeout = 2 * time.Second
targetEditorDir = ".editor"
cardinalProjectIDPlaceholder = "__CARDINAL_PROJECT_ID__"
)

type Asset struct {
BrowserDownloadURL string `json:"browser_download_url"`
}

type Release struct {
Name string `json:"name"`
Assets []Asset `json:"assets"`
}

func SetupCardinalEditor() error {
configDir, err := globalconfig.GetConfigDir()
if err != nil {
return err

Check warning on line 40 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L37-L40

Added lines #L37 - L40 were not covered by tests
}

editorDir, err := downloadReleaseIfNotCached(configDir)
if err != nil {
return err

Check warning on line 45 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L43-L45

Added lines #L43 - L45 were not covered by tests
}

// rename version tag dir to .editor
err = copyDir(editorDir, targetEditorDir)
if err != nil {
return err

Check warning on line 51 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L49-L51

Added lines #L49 - L51 were not covered by tests
}

// rename project id
// "ce" prefix is added because guids can start with numbers, which is not allowed in js
projectID := "ce" + strippedGUID()
err = replaceProjectIDs(targetEditorDir, projectID)
if err != nil {
return err

Check warning on line 59 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L56-L59

Added lines #L56 - L59 were not covered by tests
}

return nil

Check warning on line 62 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L62

Added line #L62 was not covered by tests
}

func downloadReleaseIfNotCached(configDir string) (string, error) {
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, latestReleaseURL, nil)
if err != nil {
return "", err

Check warning on line 68 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L68

Added line #L68 was not covered by tests
}

client := &http.Client{
Timeout: httpTimeout,
}
resp, err := client.Do(req)
if err != nil {
return "", err

Check warning on line 76 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L76

Added line #L76 was not covered by tests
}
defer resp.Body.Close()

var release Release
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", err

Check warning on line 83 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L83

Added line #L83 was not covered by tests
}
if err = json.Unmarshal(bodyBytes, &release); err != nil {
return "", err

Check warning on line 86 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L86

Added line #L86 was not covered by tests
}

editorDir := filepath.Join(configDir, "editor")

targetDir := filepath.Join(editorDir, release.Name)
if _, err = os.Stat(targetDir); os.IsNotExist(err) {
return targetDir, downloadAndUnzip(release.Assets[0].BrowserDownloadURL, targetDir)
}

return targetDir, nil

Check warning on line 96 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L96

Added line #L96 was not covered by tests
}

func downloadAndUnzip(url string, targetDir string) error {
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
if err != nil {
return err

Check warning on line 102 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L102

Added line #L102 was not covered by tests
}

client := &http.Client{
Timeout: httpTimeout + 10*time.Second,
}
resp, err := client.Do(req)
if err != nil {
return errors.New(url)

Check warning on line 110 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L110

Added line #L110 was not covered by tests
}
defer resp.Body.Close()

tmpZipFileName := "tmp.zip"
file, err := os.Create(tmpZipFileName)
if err != nil {
return err

Check warning on line 117 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L117

Added line #L117 was not covered by tests
}
defer file.Close()

_, err = io.Copy(file, resp.Body)
if err != nil {
return err

Check warning on line 123 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L123

Added line #L123 was not covered by tests
}

if err = unzipFile(tmpZipFileName, targetDir); err != nil {
return err

Check warning on line 127 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L127

Added line #L127 was not covered by tests
}

return os.Remove(tmpZipFileName)
}

func unzipFile(filename string, targetDir string) error {
reader, err := zip.OpenReader(filename)
if err != nil {
return err

Check warning on line 136 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L136

Added line #L136 was not covered by tests
}
defer reader.Close()

// save original folder name
var originalDir string
for i, file := range reader.File {
if i == 0 {
originalDir = file.Name
}

src, err := file.Open()
if err != nil {
return err

Check warning on line 149 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L149

Added line #L149 was not covered by tests
}
defer src.Close()

filePath, err := sanitizeExtractPath(filepath.Dir(targetDir), file.Name)
if err != nil {
return err

Check warning on line 155 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L155

Added line #L155 was not covered by tests
}
if file.FileInfo().IsDir() {
err = os.MkdirAll(filePath, 0755)
if err != nil {
return err

Check warning on line 160 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L160

Added line #L160 was not covered by tests
}
continue
}

dst, err := os.Create(filePath)
if err != nil {
return err

Check warning on line 167 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L167

Added line #L167 was not covered by tests
}
defer dst.Close()

_, err = io.Copy(dst, src) //nolint:gosec // zip file is from us
if err != nil {
return err

Check warning on line 173 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L173

Added line #L173 was not covered by tests
}
}

if err = os.Rename(filepath.Join(filepath.Dir(targetDir), originalDir), targetDir); err != nil {
return err

Check warning on line 178 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L178

Added line #L178 was not covered by tests
}

return nil
}

func sanitizeExtractPath(dst string, filePath string) (string, error) {
dstPath := filepath.Join(dst, filePath)
if strings.HasPrefix(dstPath, filepath.Clean(dst)) {
return dstPath, nil
}
return "", fmt.Errorf("%s: illegal file path", filePath)

Check warning on line 189 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L189

Added line #L189 was not covered by tests
}

func copyDir(src string, dst string) error {
srcDir, err := os.ReadDir(src)
if err != nil {
return errors.New(src)

Check warning on line 195 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L195

Added line #L195 was not covered by tests
}

if err := os.MkdirAll(dst, 0755); err != nil {
return err

Check warning on line 199 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L199

Added line #L199 was not covered by tests
}

for _, entry := range srcDir {
srcPath := filepath.Join(src, entry.Name())
destPath := filepath.Join(dst, entry.Name())

if entry.IsDir() {
// Recursively copy dirs
if err := copyDir(srcPath, destPath); err != nil {
return err

Check warning on line 209 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L209

Added line #L209 was not covered by tests
}
} else {
if err := copyFile(srcPath, destPath); err != nil {
return err

Check warning on line 213 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L213

Added line #L213 was not covered by tests
}
}
}

return nil
}

func copyFile(src, dest string) error {
srcFile, err := os.Open(src)
if err != nil {
return err

Check warning on line 224 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L224

Added line #L224 was not covered by tests
}
defer srcFile.Close()

destFile, err := os.Create(dest)
if err != nil {
return err

Check warning on line 230 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L230

Added line #L230 was not covered by tests
}
defer destFile.Close()

_, err = io.Copy(destFile, srcFile)
if err != nil {
return err

Check warning on line 236 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L236

Added line #L236 was not covered by tests
}

return nil
}

func replaceProjectIDs(editorDir string, newID string) error {
assetsDir := filepath.Join(editorDir, "assets")
files, err := os.ReadDir(assetsDir)
if err != nil {
return err

Check warning on line 246 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L246

Added line #L246 was not covered by tests
}

for _, file := range files {
if !file.IsDir() && strings.HasSuffix(file.Name(), ".js") {
content, err := os.ReadFile(filepath.Join(assetsDir, file.Name()))
if err != nil {
return err

Check warning on line 253 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L253

Added line #L253 was not covered by tests
}

newContent := strings.ReplaceAll(string(content), cardinalProjectIDPlaceholder, newID)

err = os.WriteFile(filepath.Join(assetsDir, file.Name()), []byte(newContent), 0600)
if err != nil {
return err

Check warning on line 260 in common/teacmd/editor.go

View check run for this annotation

Codecov / codecov/patch

common/teacmd/editor.go#L260

Added line #L260 was not covered by tests
}
}
}

return nil
}

func strippedGUID() string {
u := uuid.New()
return strings.ReplaceAll(u.String(), "-", "")
}
Loading