Learn to Code via Tutorials on Repl.it!

← Back to all posts
How to make a package manager (for a custom language!) — ALL PARTS
fuzzyastrocat (1845)

So — you built a custom language, and you're pretty satisfied with it. Now, where to go next? One apparent option is making a package manager for your language. But if you're like I was, you probably aren't entirely sure just how to go about making one. So, this tutorial is aimed at getting your custom package manager (for your custom lang) ready and running.

Structure

First, let's go over exactly how a package manager works. There are two main "entities" in the package manager:

  • The CDN, or Content Delivery Network, which stores all the actual data for the packages
  • The command-line tool, which queries the CDN to find the package you request

So, if I say some-manager -install cool_package, the following steps (or something similar) will occur:

  1. The command-line tool will make a request to the CDN, asking "where can I find the latest version of this package?"
  2. The CDN will respond with the location of the package data it hosts.
  3. The command-line tool will then fetch the data.
  4. The command-line tool will install the data in some directory where your language will look for packages, say ./pkg_data.

Got it? Great! Let's start coding. For this part of the tutorial, we'll be implementing the CDN (as the title advertises). Next time, we'll implement the command line tool.

Implementation of the CDN

Let's go over our task for this section:

  1. We need to make something which can store and find files, sorted by their various packages.
  2. We need to make something which will respond to HTTP requests. There are three possible "types" of requests:
    1. /package/<packagename> should all the versions of the package.
    2. /package/<packagename>/<version> should give an index of all the files in that package.
    3. /package/<packagename>/<version>/<file> should give each specific file.

Whew, that's a lot! So, let's tackle this step-by-step. (You can follow along at https://repl.it/@fuzzyastrocat/pkg-cdn if you like.)

I'm going to use nodeJS for this, since it's easy to manage files and with express serving requests is easy as well. However, you can use the language of your choosing, just note that you might have to translate some things.

First, let's make a folder called packagedata. This is where all the packages will be stored. This tutorial will not cover how those packages get there (i.e, getting published like npm does), for now we'll just put a static package there as an example. Speaking of which, let's make a folder inside packagedata named example. Inside this folder will be all the versions of this package called example. So finally, inside./packagedata/example let's add a folder named "1.0.0".

Your filetree should now be:

folder packagedata
    - folder example
        - folder 1.0.0

Inside "1.0.0", we'll put a file. Let's call it main.lang (.lang can be replaced with your custom lang's extension, I'm just using it as a placeholder.) This will be the file served by our CDN when the command-line tool requests /example/1.0.0/main.lang.

That was a lot of setup, if you're lost just check the repl for this section and update yours to match it. Okay, now the fun part — code.

In index.js (which I'm assuming repl has created for you), let's set up our basic web server:

const express = require('express'),
      app = express();

app.get('/', (req, res) => {
  res.send('Yay it works!')
});

app.listen(3000, () => {
  console.log('CDN running');
});

For those familiar with express, this should look familiar. If you're not, all you need to know for this tutorial is that app.get('/some/path/', (req, res) => {...}); is how you handle requests to your server. To make sure everything is working, run the repl and go to its address (since mine is named pkg-cdn, I go to https://pkg-cdn.fuzzyastrocat.repl.co). You should see "Yay, it works!".

Okay, now we can start implementing functionality. Let's start by getting all of our packages and creating a list of them. After app = express();, write:

const fs = require('fs'),
      packageList = fs.readdirSync("./packagedata/");

Here we're just getting all the files inside "packagedata" so that we know what packages we have. Great! Now we can properly know which packages exist. After app.get('/', (req, res) => {...});, let's write the following:

app.get('/package/:packagename', (req, res) => {
    if(packageList.includes(req.params.packagename)) res.send("This package exists");
    else res.send("This package does not exist.");
});

This will serve requests like /package/my_package and grab the name of the package we're trying to find (in the example, my_package). Then, we see if it's in our list of packages.

Let's look at what we have so far:

const express = require('express'),
      app = express();

const fs = require('fs'),
      packageList = fs.readdirSync("./packagedata/");

app.get('/', (req, res) => {
  res.send('Yay it works!')
});

app.get('/package/:packagename', (req, res) => {
    if(packageList.includes(req.params.packagename)) res.send("This package exists");
    else res.send("This package does not exist.");
});

app.listen(3000, () => {
  console.log('CDN running');
});

If we try to go to https://your-url.repl.co/package/test, it should correctly respond with This package does not exist.. But if we go to https://your-url.repl.co/package/example, it should say This package exists. If your code does this, then we can move on to the next step — giving version data.

Under where we define packageList, let's put the following:

const packages = {};

for(let packagename of packageList){
  packages[packagename] = {};
  
  for(let version of fs.readdirSync("./packagedata/" + packagename + "/")){
    packages[packagename][version] = [];
    for(let file of fs.readdirSync("./packagedata/" + packagename + "/" + version + "/")){
      packages[packagename][version].push(file);
    }
  }
}

This essentially converting the filetree to a JSON structure — after this operation is performed, packages will be the following:

{
    "example": {
        "1.0.0": ["main.lang"]
    }
}

Now, we can change our request code:

app.get('/package/:packagename', (req, res) => {
  const name = req.params.packagename;
  if(packageList.includes(name)) res.send(packages[name]);
  else res.status(404).send("This package does not exist.");
});

Here's the completed code for this section:

const express = require('express'),
      app = express();

const fs = require('fs'),
      packageList = fs.readdirSync("./packagedata/");

const packages = {};

for(let packagename of packageList){
  packages[packagename] = {};
  
  for(let version of fs.readdirSync("./packagedata/" + packagename + "/")){
    packages[packagename][version] = [];
    for(let file of fs.readdirSync("./packagedata/" + packagename + "/" + version + "/")){
      packages[packagename][version].push(file);
    }
  }
}

app.get('/', (req, res) => {
  res.send('Yay it works!')
});

app.get('/package/:packagename', (req, res) => {
  const name = req.params.packagename;
  if(packageList.includes(name)) res.send(packages[name]);
  else res.status(404).send("This package does not exist.");
});

app.listen(3000, () => {
  console.log('CDN running');
});

Now, when we go to https://your-url.repl.co/packages/test we should get the same error, but if we go to https://your-url.repl.co/packages/example we should get {"1.0.0":["main.lang"]}. Hooray, now we can finish the basic serving mechanism.

Right above app.listen(3000, () => {..., we add the following:

app.get('/package/:packagename/:version', (req, res) => {
  const name = req.params.packagename,
        version = req.params.version;
  if(packageList.includes(name) && packages[name][version]) res.send(packages[name][version]);
  else res.status(404).send("This package or version does not exist.");
});

app.get('/package/:packagename/:version/:file', (req, res) => {
  const name = req.params.packagename,
        version = req.params.version,
        file = req.params.file;
  if(packageList.includes(name) && packages[name][version] && packages[name][version].includes(file)) res.send(fs.readFileSync("./packagedata/" + name + "/" + version + "/" + file, "utf8"));
  else res.status(404).send("This package or version or file does not exist.");
});

I won't explain this in detail — it's just an extension of the /package/:packagename request mechanism. To test, first add something in main.lang (some text, any text) and then run the repl. Then try going to https://your-url.repl.co/package/example/1.0.0/main.lang: that text should be shown! If you change any part of the url though, it should say something like "package or file does not exist" depending on what you change.

If that works, hooray! We've just created our CDN. (Complete code can be found in the example repl, in the file index.js.)

Conclusion

We've now implemented a basic CDN. This is just a starting point — right now it's very basic, and later we may adapt and improve it.

Stay tuned for part 2, where we'll implement the command-line tool and actually make our packager!

Part 2

I'm merging these tutorials for easy reading. So, here's Part 2.

Recap

Let's quickly go over what we've done so far.

  • We've built a CDN, which stores package data and will deliver it to our tool.
  • Our CDN responds to GET requests on the url /package/<packagename>/<version>/<file>. Any invalid fields will result in a 404 error and a message such as "This package does not exist".

So, our task at hand: build a command-line tool which gets the package data from the CDN and places it in a nice location. For this tutorial we'll use ./pkg_data/.

Let's get started! (The example repl can be found at https://repl.it/@fuzzyastrocat/cmd-line. As with before, feel free to follow along if you like.)

Language Choice

For this section, I'm going to use Go. I'm using Go because of two reasons:

  1. Go works nicely for HTTP things
  2. Go is compiled

This second one is especially important here — we need our tool to be compiled since we want to be able to create an executable from it. So, as with last time, it's okay if you want to use a different language, but consider carefully based on the two criteria above.

Implementation of the Command-Line Tool

First things first, we need to set up our repl.it workspace. Make a new Go project. Inside our new Go project, create a file called .replit. This is a magical special file that repl.it looks at when you click the run button. In it, put the following:

language = "go"
run = "go build -o packager -ldflags \"-s -w\" main.go"

This tells repl.it that our repl's main language is Go and that when we click the run button it should execute go build -o packager -ldflags \"-s -w\" main.go. (The ldflags just tell the go compiler not to generate version or debug info embedded in the executable. We don't really need to worry about that info since this will become a standalone executable.) -o packager tells Go that we want to output our new executable to ./packager. DO NOT remove this or use -o main: repl will hide an executable called ./main by default, so you won't realize that the program has compiled!

The .replit file overrides repl's default behavior, which is to run the go file. Instead, we simply build it, meaning that an executable (./packager) is generated. We can then run this executable by typing ./packager in the shell (on the right). When you are done with your packager, this will be the actual tool — that way, you can run it without any need for the go compiler.

Okay, now that we've set up our project let's start implementing our interface. The first thing we need to do is check what package the user wants. When you run the executable like ./packager install some_package, ./packager, install and some_package will get given to Go in the form of an array called os.Args. To use it, we need to import os. Let's make a quick program that just echoes the arguments we give it:

package main

import (
  "fmt"
  "os"
)

func main() {
  for _, name := range os.Args[1:] {
    fmt.Println(name)
  }
}

Now, click the Run button of your repl. Wait for it to finish running — you should see a file named "packager" get generated. Once it's done building, type ./packager these are some arguments into the shell on the left — the program should print those arguments back to you.

"Wait, why do we ignore the first argument?" Remember that the first argument will always be ./packager, and we don't care about the name of the program.

Okay, let's do something more useful with those arguments. We'll expect arguments of the following form:

./packager install [packagename]

We'll start by making the only option install — we include this only for later extensibility. Let's simply start by printing out the available versions of a package:

package main

import (
  "fmt"
  "os"
  "net/http"
  "io/ioutil"
)

func main() {
  if os.Args[1] != "install" {
    fmt.Println("Invalid option " + os.Args[1] + "!")
    os.Exit(1)
  }

  packagename := os.Args[2]

  resp, _ := http.Get("https://your-cdn-url.repl.co/package/" + packagename)
  if resp.StatusCode == 404 {
    fmt.Println("That package does not exist!")
    os.Exit(1)
  }

  body, _ := ioutil.ReadAll(resp.Body)
  fmt.Println(string(body));
}

Obviously, replace "your-cdn-url.repl.co" with your actual cdn url from the last tutorial.
Run the repl, and try ./packager install foo. It should respond with "That package does not exist!". Try again with ./packager install example (assuming that you followed the previous tutorial), and it should print out ["1.0.0"].

Okay, now we need to figure out how to parse this array. We'll make use of Go's encoding/json package, since it parses JSON arrays nicely:

package main

import (
  "fmt"
  "os"
  "net/http"
  "io/ioutil"
  "encoding/json"
)

func main() {
  if os.Args[1] != "install" {
    fmt.Println("Invalid option " + os.Args[1] + "!")
    os.Exit(1)
  }

  packagename := os.Args[2]

  resp, _ := http.Get("https://pkg-cdn.fuzzyastrocat.repl.co/package/" + packagename)
  if resp.StatusCode == 404 {
    fmt.Println("That package does not exist!")
    os.Exit(1)
  }

  versions, _ := ioutil.ReadAll(resp.Body)
  var versions_arr []string;
  json.Unmarshal(versions, &versions_arr)
  fmt.Printf("Versions: %v\n", versions_arr)
}

Note that versions is a []byte, which is what json.Unmarshal takes as an argument, so we don't do any string() conversion. If all goes correctly, ./packager install example should now print out "Versions: [1.0.0]".

Now we need to figure out how to find the latest version. We'll assume that all versions all well-formed sequences of 3 numbers separated by a . (things like 1.2.3 and 2.10.0 are fine but 0..0 and 0.5 are not), which makes writing a compare function easier:

func later_version(a string, b string) string {
  sa := strings.Split(a, ".")
  sb := strings.Split(b, ".")
  for i := range sa {
    ia, _ := strconv.Atoi(sa[i])
    ib, _ := strconv.Atoi(sb[i])
    if(ia > ib){ return a }
    if(ib > ia){ return b }
  }
  
  return a
}

Now, we just need to iterate through the array of versions to find the latest (we assume there is at least one version):

largest := versions_arr[0]
for i := range versions_arr {
  largest = later_version(largest, versions_arr[i])
}

So now we can tie it all together to find the files in the package:

package main

import (
  "fmt"
  "os"
  "net/http"
  "io/ioutil"
  "encoding/json"
  "strings"
  "strconv"
)

func later_version(a string, b string) string {
  sa := strings.Split(a, ".")
  sb := strings.Split(b, ".")
  for i := range sa {
    ia, _ := strconv.Atoi(sa[i])
    ib, _ := strconv.Atoi(sb[i])
    if(ia > ib){ return a }
    if(ib > ia){ return b }
  }
  
  return a
}

func main() {
  if os.Args[1] != "install" {
    fmt.Println("Invalid option " + os.Args[1] + "!")
    os.Exit(1)
  }

  packagename := os.Args[2]

  resp, _ := http.Get("https://your-cdn-url.repl.co/package/" + packagename)
  if resp.StatusCode == 404 {
    fmt.Println("That package does not exist!")
    os.Exit(1)
  }

  versions, _ := ioutil.ReadAll(resp.Body)
  var versions_arr []string;
  json.Unmarshal(versions, &versions_arr)

  latest := versions_arr[0]
  for i := range versions_arr {
    latest = later_version(latest, versions_arr[i])
  }

  resp, _ = http.Get("https://your-cdn-url.repl.co/package/" + packagename + "/" + latest)

  files, _ := ioutil.ReadAll(resp.Body)
  fmt.Println(string(files))
}

Once again, replace your-cdn-url.repl.co with your actual cdn url. If running ./packager install example prints out ["main.lang"] (or whatever your example filename was), great! We can now move on to actually (finally!) fetching the files.

First, we need to parse the array. Then, for each file in the array, we need to fetch (http GET) the contents of the file, which we can then save in a local directory. First though, we'll just print the contents of each file. Replacing fmt.Println(string(files)), here is the relevant code to complete this task:

var files_arr []string;
json.Unmarshal(files, &files_arr)

for _, file := range files_arr {
  resp, _ = http.Get("https://your-cdn-url.repl.co/package/" + packagename + "/" + latest + "/" + file)

  contents, _ := ioutil.ReadAll(resp.Body)
  fmt.Println(string(contents))
}

(Replace your-cdn-url.repl.co as needed.) I won't explain this in detail because there's really no new code here. We're just doing what we've done before, except extended slightly.

So... try it out! Last tutorial, I put Test file! as the contents of main.lang. Therefore, I should expect Test file! to be printed to the console (which it was). If your program does this correctly, then we can move on to the final section: doing something useful with this file data.

First of all, we need to create a folder called ./pkg_data if it doesn't exist. This is easy with os.Mkdir:

os.Mkdir("./pkg_data", 0755)

(This code goes at the top of main(), you'll see it in the completed code linked below.) 0755 is a permissions number, it means that you'll be able to do anything with the folder and other users can only read it and enter it (but not modify it).

After we get the packagename from os.Args, we need to create a folder for the package. We can do that in the same way as above, with os.Mkdir:

os.Mkdir("./pkg_data/" + packagename, 0755)

Now we can finally write the files into our local package location. We can write to and make a file with os.Create:

file, _ := os.Create("./moduledata/" + packagename + "/" + file)
file.Write(contents)
defer file.Close()

(This replaces fmt.Println(string(contents)).) Okay, the big moment has come: try building and running ./packager install example. If all goes well, you should have a file main.lang inside ./moduledata/example/, with the contents you put in your CDN!

Completed code can be found here. Obviously, replace all occurrences of pkg-cdn.fuzzyastrocat.repl.co with your CDN url.

Conclusion

Wow, that was a lot of work for an utterly trivial result! But now, we do have a minimal package manager which can fetch and "clone" packages from our CDN. Play around with the CDN — try adding more files to the example package and use your command line tool to fetch them. Just remember to re-run your CDN (not the command line tool!) every time you add or change hosted package files. (Note that right now our install option both installs and/or updates, we'll separate out that duty later.)

The nice thing about using a compiled language is that you can take the ./packager executable, put it anywhere (within the same type of system, obviously a Windows-generated executable won't work on a Mac), rename it, do whatever (without modifying the contents), and it will still work the same way. It's a standalone executable!

Next time, we'll implement more functionality for our command line (like update and remove) and make it a truly usable package manager!

Part 3

And now, for part 3:

The task

For this tutorial, we'll be wrapping up the loose ends. We'll do the following:

  • Finish our command line tool: give it update and remove functionality
  • Add to our CDN: Allow users to publish packages to it
  • Add accounts

Setup

The example repls for this section can be found here and here if you'd like to follow along. (They're new repls since we'll be changing both aspects of our package manager.) There's not much setup this time since we're just expanding our existing work.

In contrast to the previous parts, we'll be adding lots of stuff in different places. So, I would highly recommend cloning both example repls, and then just following-along with this tutorial. (Use this like a code-guide of how I got to the finished project)

Adding functionality to the command-line tool

First, let's allow our users to do something else than install:

if os.Args[1] != "install" && os.Args[1] != "update" && os.Args[1] != "remove" {
  fmt.Println("Invalid option " + os.Args[1] + "!")
  os.Exit(1)
}

(This replaces the original install check.) Now we need to handle each of these options. We can actually handle all three cases "at the same time". Under packagename := os.Args[2] we add the following:

if _, err := os.Stat("./pkg_data/" + packagename); os.IsNotExist(err) {
  if os.Args[1] == "update" || os.Args[1] == "remove" {
    fmt.Println("Package " + packagename + " is not installed!")
    os.Exit(1)
  }
} else if os.Args[1] == "install" {
  fmt.Println("Package " + packagename + " is already installed!")
  os.Exit(1)
}

You can probably reason out what this does: if the package's folder doesn't exist and we're trying to update or remove it, we say that it isn't installed and exit. If it does, and we're trying to install it, we say that it's already installed and exit. There's probably a more elegant way to do this, but it works.

Note: I'm using nested if's for a reason! If the inner if was combined with the outer if, the else if clause would behave differently.

Now let's handle remove's functionality (recall that our install behaved like an update or install, so we've already taken care of the functionality for those two). Under the code we just added, add the following:

if os.Args[1] == "remove" {
  os.RemoveAll("./pkg_data/" + packagename)
  os.Exit(0)
}

Easy! We've now completed task 1: test out ./packager update ..., ./packager install ..., and ./packager remove ... to make sure the changes work. It should give you an error if you try to do something invalid, ie update a package which isn't installed or install a package which is already installed.

Adding to the CDN: Publishing functionality

Okay, now head on over to your CDN. First, we define a function updatePackages so that we can update the package list once a new package is published. If you didn't sync with the example repl, I'd suggest doing that now. updatePackages replaces some code around it, which could be confusing to update and lead to wrong code.

We need to respond to a new kind of request — an HTTP POST request. POST requests have the url and a "body" which contains the important data. We handle our request like so:

app.post('/publish/:packagename', (req, res) => {
  const name = req.params.packagename,
        data = req.body;

  fs.mkdirSync('./packagedata/' + name, { recursive: true }); // Recursive mimics -p

  try {
    fs.mkdirSync('./packagedata/' + name + '/' + data.version);
  } catch (e){
    res.send("VERSION_EXISTS");
    return
  }

  for(let file in data){
    if(file === "version") continue;

    fs.writeFileSync("./packagedata/" + name + '/' + data.version + "/" + file.slice(1), data[file], "utf8");
  }
  
  updatePackages();
  res.send(); // Acknowlege the request was OK with an empty 200 response
})

This is placed after app.get('/', (req, res) => { ... });. The code here responds to POST requests on /publish/<some_package_name>. It looks at the body, which should be JSON, makes the necessary directory for the package (./packagedata/<some_package_name>) and makes a folder with the new version (./packagedata/<some_package_name>/{data.version}). Finally, we loop through all the keys in the JSON object (which are not the version key) and make a new file for each with the contents of the file equal to the value of that key. We use file.slice(1) since each filename will be prefixed by a _, to differentiate a file named "version" from the actual version number.

To parse the body as JSON, we need to add a new dependency to our project. Below app = express(), we write the following:

const bodyParser = require('body-parser');
app.use(bodyParser.json());

This does what you probably think it does — allows the app to parse JSON in a POST request body.

Command-line updates

Now let's make updates on the command-line side so that we can actually publish things. We add || os.Args[1] != "publish" to our initial if-statment check, add "bytes" to our imports, and add the following after packagename := os.Args[2] (check the completed code if you are lost):

if os.Args[1] == "publish" {
  jsonmap := map[string]string {
    "version": os.Args[3],
  }

  files, _ := ioutil.ReadDir("./" + packagename)

  for _, f := range files {
    dat, _ := ioutil.ReadFile("./" + packagename + "/" + f.Name())
    jsonmap["_" + f.Name()] = string(dat)
  }

  requestBody, _ := json.Marshal(jsonmap)

  res, _ := http.Post("https://your-cdn-url.repl.co/publish/" + packagename, "application/json", bytes.NewBuffer(requestBody))

  respcode, _ := ioutil.ReadAll(res.Body);
  respstr := string(respcode)

  if respstr == "VERSION_EXISTS" {
    fmt.Println("That package version already exists!")
    os.Exit(1)
  }

  fmt.Println("Package published.")

  defer res.Body.Close()
  os.Exit(0)
}

So, if you use ./packager publish test 1.0.0, it will look in the directory ./test, take all the files from there and publish them to the CDN (with version 1.0.0). Let's make a directory, ./test, and put a few files inside. Then try ./packager publish test 1.0.0! It should say "Package published successfully", and when you go to your-cdn.repl.co/package/test, it should give you ["1.0.0"] instead of "This package does not exist". Great!

Finishing up

There's one problem with our system so far: anyone can update a package, regardless of who first made it! Let's fix that.

Inside ./test, we add a file .user. Inside that file we put me or whatever name you like. This will be your "user" that gets published with the package to the CDN. Now, on the CDN, we add logic. We replace our original app.post handler with the following:

app.post('/publish/:packagename', (req, res) => {
  const name = req.params.packagename,
        data = req.body;

  try {
    fs.mkdirSync('./packagedata/' + name);
  } catch (e){
    if (data[".user"] !== fs.readFileSync('./packagedata/' + name + '/.user', 'utf8')){
      res.send("PACKAGE_EXISTS")
      return
    }
  }

  try {
    fs.mkdirSync('./packagedata/' + name + '/' + data.version);
  } catch (e){
    res.send("VERSION_EXISTS");
    return
  }

  fs.writeFileSync("./packagedata/" + name + "/.user", data[".user"], "utf8")

  for(let file in data){
    if(file === "version" || file === ".user") continue;

    fs.writeFileSync("./packagedata/" + name + '/' + data.version + "/" + file.slice(1), data[file], "utf8");
  }
  
  updatePackages();
  res.send(); // Acknowlege the request was OK with an empty 200 response
})

Now, we need to handle PACKAGE_EXISTS from the command line. After if respstr == "VERSION_EXISTS" { ... }, we put the following:

if respstr == "PACKAGE_EXISTS" {
  fmt.Println("That package already exists!")
  os.Exit(1)
}

Great! If you still haven't, you probably want to sync-up with the example repl since there's been so many changes (some of which might not have been covered here).

Now, try this: publish your package "test" again (with the new .user file). Make sure that works without saying "That package already exists!". If it says that the version exists, just change the version (you've already published that version). Once you've successfully published your package with the .user, try changing the .user file and the version and uploading again. This should say "That package already exists!", since you are "a different user".

As always, check the example repls for complete code.

Next steps

That's the end of this tutorial series!

But don't stop here! There's a lot you could do to improve it. The first thing would be to add passwords to your user system. This is a fairly easy addition that will add more (but not complete) security to your packager. Next, I'd add friendlier errors. For instance, try ./packager with no arguments. (Oh no, a Go panic!) This is another easy addition that will make your packager much more refined. Then, there are a few more complex additions you could try:

  • Add a login system through your packager tool itself. Make a command, ./packager login [username] [password] that would store your user data somewhere. Then, rather than having to create a .user file every time you want to make a package, you could just run ./packager init and it would read from the stored user data and put that in a new .user file.
  • Make sure all versions are correct. To do this, you could take npm's route: instead of ./packager publish test 1.5.2, you just do ./packager publish test [major or minor or patch] and it takes care of generating the version for you.
  • Allow folders in a package. Right now we only take the files in the current directory, add a function to recursively traverse the directories too and send that through the JSON.
  • Add an unpublish function. Remove the directory, then call updatePackages().

There's so much more to add — the only limit is your imagination!

Conclusion

I hope you've enjoyed these tutorials (I sure have), and I hope they've helped. If you have any questions, please ask!

Comments
hotnewtop
ELDER054 (29)

I'm getting an error in my repl. It says
Cannot find module 'express'

fuzzyastrocat (1845)

@ELDER054 Replit might have changed their auto-install capability, try installing the express package with npm install express (in the shell) first and see if that works.

ELDER054 (29)

Ok, express works, but now i'm getting
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x10 pc=0x65d5e0]

goroutine 1 [running]:
main.main()
/home/runner/cmd-line/main.go:39 +0x220
@fuzzyastrocat

fuzzyastrocat (1845)

@ELDER054 I don't see this error when I run your repl, did you resolve it?

ELDER054 (29)

It's in the Go repl. If I type ./packager install example, it gives me that error @fuzzyastrocat

fuzzyastrocat (1845)

@ELDER054 Well given the line number (39) I think I've found your problem. Try going to https://pkg-cdn.ellder054.repl.co/package/ and see what happens. Looks like you wrote one too many l's in your username.

TravisRaney (11)

accept my team invitation

fuzzyastrocat (1845)

@TravisRaney Why should I? I've been given no information about the team. If I want to join the team I will — but since I know nothing about it, I can't make a decision.

TravisRaney (11)

@fuzzyastrocat look I got you on my team deal and its saying pending okay

fuzzyastrocat (1845)

@TravisRaney That statement is incorrect. You have asked me to be on your team — it is up to me to decide whether I want to accept. I'd like to know more about the team before I randomly and blindly accept a team invite.

TravisRaney (11)

@fuzzyastrocat well get this that $100,000 grand prize or something about the language jams so that is the reason why I am creating a team

fuzzyastrocat (1845)

@TravisRaney That was two months ago. The jam has been over for quite some time.

HahaYes (1864)

This is a really nice tutorial. Maybe combine 1 and 2? But this is super long so I wouldn't consider this cycle squeezing.

fuzzyastrocat (1845)

@HahaYes Thanks! When I write part 2, yes I might combine them. The reason I was thinking of separating them is because part 2 will have a lot of code, so it will be pretty long... but I might combine them.

HahaYes (1864)

@fuzzyastrocat yep. I don't know whether to learn Javascript or C#. Completely random but whatever. I might consider game dev soon

fuzzyastrocat (1845)

@HahaYes Learning js will be easier. Writing JS will also probably be easier. I personally find that I can produce working code quickest in JS, with Python coming in a close second place. There are some good libraries for game dev in JS — I personally like p5.js for 2D and three.js for 3D. However, if you're thinking of using a game engine (unity, etc) then you'll definitely want to learn C#.

HahaYes (1864)

@fuzzyastrocat yeah... maybe I'll just learn both XD

HahaYes (1864)

@fuzzyastrocat I have some knowledge from C++, so learning another lang should be easier?

fuzzyastrocat (1845)

@HahaYes Learning new languages is always a good thing :D

fuzzyastrocat (1845)

@HahaYes C# yes, JS maybe. What other langs do you know?

HahaYes (1864)

@fuzzyastrocat uh I mean... C++ has always been my goto lang.

fuzzyastrocat (1845)

@HahaYes goto lang... heh heh
Learning C# will feel natural, since it's in the family of C-style languages. The syntax of JS will be easy, since it's very C-like, however the semantics might be a little hard at first since it's more high-level. But it shouldn't be too hard.

HahaYes (1864)

@fuzzyastrocat I personally struggled with learning with Python haha but Js seems a bit more friendly to me. C# is pretty nice. Its basically rip-off java. Heh goto get it (C)

fuzzyastrocat (1845)

@HahaYes Yeah, Python is interesting because it's a unique style (the oop paradigm in particular). JS is definitely closer to the C languages than Python. (you has partaken in the funny)

fuzzyastrocat (1845)

@HahaYes I'm glad I didn't make this a 1-parter: Part 2 is another long tutorial, and I'm still gonna need a part 3 :D

EpicGamer007 (1736)

@fuzzyastrocat , damn i have a thousand line+ tutorial coming up. lol. I already have surpassed 1100 lines(1000 are by me)

fuzzyastrocat (1845)

@EpicGamer007 In my opinion I'd split that, I feel like the audience will lose attention if it gets really long.

EpicGamer007 (1736)

@fuzzyastrocat , really? That might be a good idea... IDK

fuzzyastrocat (1845)

@EpicGamer007 I finally finished my tutorial, I'm glad it's done lol... and mine isn't even as long as yours I don't think. So yeah I think you'll probably want to split yours up

HahaYes (1864)

@fuzzyastrocat oh no don't do that mods don't like that

fuzzyastrocat (1845)

@HahaYes Replied to wrong person, sorry about that. Clarified my comment too.

HahaYes (1864)

@fuzzyastrocat ehhh its not a mass ping. But I'm neutral on this I guess

fuzzyastrocat (1845)

@HahaYes Wait, what are you responding to?

fuzzyastrocat (1845)

@HahaYes oh ok, makes more sense now

DungeonMaster00 (190)

@HahaYes c# has a better standard library and its beats java most of the time

DungeonMaster00 (190)

@HahaYes it also has great documentation

DungeonMaster00 (190)

@DungeonMaster00 there are also many other great things i have not mentioned

DynamicSquid (4937)

Wow! Me and firefish were actually working on one (and when I say "we" I mean he did all the work cause I clueless) and this cleared up a lot of things (even though we're doing it in Go)! Can't wait for part 2!

fuzzyastrocat (1845)

@DynamicSquid Nice timing! (Great to know you liked it! Part 2 will be coming shortly!)
(Also, I feel like we have some psychic connection or something... I was thinking of using Go for the command-line tool since it's compiled and works well for http stuff... wow)

HahaYes (1864)

@DynamicSquid yeah, saw what you guys are doing in github... maybe I can join?

HahaYes (1864)

@fuzzyastrocat oh no don't do that mods don't like that

fuzzyastrocat (1845)

@HahaYes Wait, why? Squid had expressed interest in part 2 so I thought I'd keep them updated... should I not?

EpicGamer007 (1736)

I am so jealous of node devs, it is so good for so many cool things. I wanna learn so bad

fuzzyastrocat (1845)

@EpicGamer007 I'd recommend it! Node is perfect for something like this, it works very nicely for web servers.

EpicGamer007 (1736)

@fuzzyastrocat , ive tried so many tutorials, but they all taught me nothing. I have learned a bit more cuz of zave who was really helpful. But I feel like I only scratched the surface. I wanna learn moreee

fuzzyastrocat (1845)

@EpicGamer007 Unfortunately, I think the best way to learn node is just to try doing things with it, and then repeatedly search stackoverflow until you find what you need to solve the problems :D That's basically how I learned it, just a lot of experience and a lot of googling.

EpicGamer007 (1736)

@fuzzyastrocat , hmmmmm, i see. Thanks for the info!

fuzzyastrocat (1845)

@EpicGamer007 No problem! If you have any node-related questions feel free to ask me.

Codemonkey51 (1057)

Lol imagine using node I have a really cooler idea for a package manager but you'll have to wait until it is released to find my idea out @fuzzyastrocat
Cool tutorials btw

EpicGamer007 (1736)

@Codemonkey51 , "really cooler" - Best grammarer

Can't wait for it!

Codemonkey51 (1057)

I edited the message while typing lol, get ready for the package manager for cookey lang @EpicGamer007

EpicGamer007 (1736)

@Codemonkey51 , ooooh that seems cool, a jam lang

Codemonkey51 (1057)

Lol ye I better get back to coding it @EpicGamer007

EpicGamer007 (1736)

@Codemonkey51 , good luck! Oh yea , u made the replitdb client for python right, i just wanna say thank you cause your thing really inspired me ;) I made one and it has already been used so big yay :D

fuzzyastrocat (1845)

@Codemonkey51

really cooler

I hope it is the bestest :D

Highwayman (1483)

Oo This sounds interesting! XP

fuzzyastrocat (1845)

@Highwayman Great! I'll be writing part 2 fairly soon, so hope you enjoy!
(And thanks for the upvote, this tutorial isn't way at the bottom any more :D)

Highwayman (1483)

Awesome! And yw! 😜@fuzzyastrocat