Author Avatar

Installing Swift scripts as global commands with npm

If you use Swift to create small command-line utilities, you’ve probably gotten tired of recompiling and moving the binary into your $PATH after every change.

For simple scripts, constantly recompiling tends to break the iteration flow quite a bit.

A common way to avoid compiling is to execute the script directly using the Swift interpreter, for example, for a greeting utility:

import Foundation

let name = UserDefaults.standard.string(forKey: "name") ?? "unknown"

print("Hello \(name)!")

You can run it directly without a build step:

swift ~/greeting/main.swift -name "Crisfe"

Or create some kind of alias/manual wrapper:

# ~/.zshrc or ~/.bashrc

greeting() {
    swift ~/greeting/main.swift -name $1
}

Although this works, it has some limitations:

Using npm as a lightweight package manager

A more convenient alternative is to treat the script as a binary.

npm can be used to install Swift scripts (actually scripts written in any programming language) as global commands.

This approach has several advantages:

For our greeting utility, we only need a simple structure:

greeting/
 package.json
 main.swift

Where package.json contains:

{
  "name": "greeting-swift",
  "type": "module",
  "bin": {
    "greeting": "./main.swift"
  }
}

The bin key tells npm to expose that file as an executable called greeting.

The name key defines the package name, which is useful if we later want to uninstall the utility.

Shebangs

The script needs a valid shebang so the system knows how to execute it:

#!/usr/bin/env swift

import Foundation

let name = UserDefaults.standard.string(forKey: "name") ?? "unknown"

print("Hello \(name)!")

And execution permissions:

chmod +x main.swift

Installing globally

From the project folder:

npm install -g .

npm creates a global symlink pointing to the script.

Now we can run it like any other command:

greeting -name Crisfe

Output:

Hello Crisfe!

And uninstall it with:

npm uninstall -g greeting-swift

Conclusion & Tradeoffs

This approach shines for utilities that run occasionally and change often — edit the file, and the update is instantly available system-wide with no recompilation needed.

The main tradeoff is startup time. Interpreting Swift on every run can take 2–5 seconds, which is fine for occasional use but painful for commands invoked hundreds of times a day or from automated scripts. In those cases, compiling a release binary is the right call.

That said, the two approaches aren't mutually exclusive: use the interpreted script during development, compile to a binary once the tool is stable.

Original draft written in spanish.